Tafari
Tafari

Reputation: 3089

How to make controls move/resize accordingly to background image in WPF

I haven't been doing much in WPF recently, so decided to do something I could use, so first I'm trying to make something like interactive form, to reduce usage of paper. So here it's what I'm doing, I have a scan of the original form, it's saved in .png format which I added as resource, then set it as window's background image via:

<Window.Background>
    <ImageBrush ImageSource="pictures/podstawowa_front.png" />
</Window.Background>

That works perfectly fine, but now in the place where a user is meant to enter some data, I wanted to put WPF controls, which is also fine. The problem comes when I want to resize the window, the background image resize differently than the control. I have tried using different containers for the controls but none seems to work. Also removed all height, width, alignments from the control, and left just margin (so it has some starting position).

Example:

At startup - it's more or less at correct position with similar size:

Application startup

After resizing window:

After resizing window

So you can see that I shrunk the window a bit, but the control was shrunken more and was not moved at all.

I have tried googling and looking for similar problems but found hardly anything, tried to messing up with the different containers, transformations but nothing seems to work as I'd like it to. (I'm also trying to stick just to XAML)

I probably could solve it with using lots of nested containers but I'm trying to avoid it as it seems to be an overkill and I wonder if there is some other faster and more elegant way of doing that. Feel free to ask for additional details in comments. Thanks in advance for any tips.

XAML code:

<Window x:Class="DnD.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="768" Width="1366">
    <Window.Background>
        <ImageBrush ImageSource="pictures/basic_front.png" />
    </Window.Background>

    <Grid>
        <TextBox Margin="265,307,970,413" TextWrapping="Wrap" Text="TextBox" FontSize="10"/>
    </Grid>

</Window>

Upvotes: 2

Views: 1834

Answers (2)

Tafari
Tafari

Reputation: 3089

Well It's not 100% what I've wanted to achieve but this closes thing I've managed to do (thanks to JNP for tip about margin binding). So basically what I'm doing is to use multiconverter for margin bindings, while passing the starting position of the control as ConverterParameter, margins are depended on window's height and width ratio change (this is the only flow, as it seems the background image is transformed slightely different).

The final result moves and resizes the control quite nice but the more we resize the window the difference starts to be more noticable, it's probably due to mentioned transformation difference.

Code:

ImageLayout.xaml:

    Title="Character sheet - Front" Height="768" Width="1366" SizeChanged="Window_SizeChanged">
<Window.Resources>
    <local:MarginConverter x:Key="MyConverter"/>
</Window.Resources>
<Window.Background>
    <ImageBrush ImageSource="Resources/base_front.png"/>
</Window.Background>
<Grid>
    <TextBox x:Name="TextBox" TextWrapping="Wrap" Text="TextBox">
        <TextBox.Margin>
            <MultiBinding Converter="{StaticResource MyConverter}" ConverterParameter="266,307,962,408">
                <Binding RelativeSource="{RelativeSource AncestorType={x:Type Window}}" Path="WindowProperties.CurrentWidthChange" />
                <Binding RelativeSource="{RelativeSource AncestorType={x:Type Window}}" Path="WindowProperties.CurrentHeightChange"/>
            </MultiBinding>
        </TextBox.Margin>
    </TextBox>
</Grid>

As said converter takes the base margin in ConverterParameter and is bound to width/height ratio change, (which is kept in additional class, since Window is not DependencyObject).

ImageLayout.xaml.cs:

public partial class ImageLayout : Window
{
    public WindowProperties WindowProperties { get; set; }

    public ImageLayout()
    {
        WindowProperties = new WindowProperties();

        InitializeComponent();
    }

    private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        WindowProperties.CurrentHeightChange = e.NewSize.Height / 768;
        WindowProperties.CurrentWidthChange = e.NewSize.Width / 1366;
    }
}

Here we just have the instance of WindowProperties, which is class implementing INotifyPropertyChanged, additionally I've overriden an event which is called on window's size change, in order to update the ratio values of both width and height change.

WindowProperties.cs:

public class WindowProperties : INotifyPropertyChanged
{
    public WindowProperties()
    {
        CurrentHeightChange = 1;
        CurrentWidthChange = 1;
    }

    private double _currentHeightChange;
    public double CurrentHeightChange
    {
        get
        {
            return _currentHeightChange;
        }
        set
        {
            _currentHeightChange = value;
            NotifyPropertyChanged("CurrentHeightChange");
        }
    }

    private double _currentWidthChange;
    public double CurrentWidthChange
    {
        get
        {
            return _currentWidthChange;
        }
        set
        {
            _currentWidthChange = value;
            NotifyPropertyChanged("CurrentWidthChange");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void NotifyPropertyChanged(string property)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(property));
        }
    }
}

Additional class implementing INotifyPropertyChanged, needed for converter's bindings purpose.

MarginConverter.cs:

public class MarginConverter : IMultiValueConverter
{

    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        string[] positions = parameter.ToString().Split(',');
        double leftPos = double.Parse(positions[0]);
        double topPos = double.Parse(positions[1]);
        double rightPos = double.Parse(positions[2]);
        double bottomPos = double.Parse(positions[3]);

        var actualMargin = new Thickness(leftPos, topPos, rightPos, bottomPos);

        return new Thickness(actualMargin.Left * (double)values[0],
                             actualMargin.Top * (double)values[1],
                             actualMargin.Right * (double)values[0],
                             actualMargin.Bottom * (double)values[1]);
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        return null;
    }
}

Converter basically takes the starting position from parameter and then multiplies it by ratio of window's size change.

Results:

Application start:

Application start

Resize:

After resize

A lot of resizing, causing bad transformations:

Resizing flaw

Conclusion:

As you can see it works pretty good and could be use if the resize options would be limited, to make it better there should be some improvement done to ratio computation, but unfortunately I do not know what exact transformations are done to the background image when resizing the window. The improvement also seems to be bit an overkill and in general in my opinion it's better to copy the image layout just with controls without having image in the background.

Upvotes: 0

JNP
JNP

Reputation: 131

This happens because the Margin remains the same when the Window size is changed. So when you reduce the width of the Window, the TextBox remains the same distance from the left and right edges.

If you have to support resizing, you really can't do this with just XAML. If you can go beyond XAML, you could bind the Margins to the Height and Width of the Window and write a converter to change the Margins to correctly position the TextBox.

If you don't actually need to support resizing, just set the ResizeMode for the window to NoResize.

A better approach to positioning controls over an image is to use a Canvas instead of a Grid, and place the image inside the Canvas instead of as the window background. Then the TextBox can be placed in the appropriate place over the image using the Canvas properties instead of using the Margin. This won't solve the resizing problem, although this way you can leave things at their original size when the window is resized.

Upvotes: 3

Related Questions