Button with properties set in code-behind not firing click event - c#

When setting the translationX and translationY of a button in the code-behind, its click events aren't fired: the button doesn't seem to be able to be clicked. The buttons are part of a StackLayout and I have used x:Name to access them from the code-behind!
The buttons properties except for the translations are set in the XAML, the formers are set like so:
element.TranslationX = array[i].xPos;
element.TranslationY = array[i].yPos;
UPDATE
Code-Behind
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
button.Clicked += Button_Clicked;
CircleLayout(132.0, Math.PI, 7, "menuGrid");
}
public void CircleLayout(double r, double theta, int nbElements, String currentMenu)
{
CircleObject[] array = new CircleObject[nbElements];
for (int i = 1; i < nbElements; i++)
{
if (i == 0)
{
array[i] = new CircleObject(r * Math.Sin(theta), r * Math.Cos(theta));
} else
{
theta += (Math.PI / 180) * 360 / nbElements;
array[i] = new CircleObject(r * Math.Sin(theta), r * Math.Cos(theta));
}
StackLayout element = this.FindByName<StackLayout>(currentMenu + i);
element.TranslationX = array[i].xPos;
element.TranslationY = array[i].yPos;
}
}
public class CircleObject
{
public double xPos;
public double yPos;
public CircleObject(double x, double y)
{
xPos = x;
yPos = y;
}
}
private void Button_Clicked(object sender, EventArgs e)
{
//do something
}
}
Example of a button in XAML
<StackLayout x:Name="menuGrid0">
<Button x:Name="button"
BackgroundColor="#FF6633"
CornerRadius="70"
HeightRequest="65"
WidthRequest="65"
HorizontalOptions="Center"/>
<Label x:Name="textDetection"
Text="D E T E C T I O N"
FontSize="9"
TextColor="#FF6633"
HeightRequest="10"
HorizontalOptions="Center"/>
</StackLayout>

Related

Why does my scale variable not adjust after a button click in C#?

The method genereer ("generate") draws a Bitmap image of a Mandelbrot. It has a variable schaal ("scale") which makes the Bitmap zoom in on the image with a certain factor. The standard value for this is 0.01, as declared above in the partial class Form1.
I have a textBox3 in the GUI, and I want to be able to type a new value in this textBox3, click on the button and draw a new image based on the scale.
I've written a method called Lees ("read"), which parses a double from textBox3. I've put this method in the Button1_MouseClick method, as well as the genereer method to draw a new image upon button click. I've also put the genereer method in the Form1_Shown method.
However, this doesn't seem to be working. When I click on the button, no matter what the value is, I get a white dot in the middle of the screen.
public partial class Form1 : Form
{
public double waardeX, waardeY, schaal = 0.01;
public int maxIteraties;
public Form1()
{
InitializeComponent();
}
private void Form1_Shown(object sender, EventArgs e)
{
Genereer();
}
public void Lees()
{
schaal = double.Parse(textBox3.Text);
}
private void Genereer()
{
double max = 100;
Bitmap bitmap = new Bitmap(Figuur.Width, Figuur.Height);
for (int x = 0; x < Figuur.Width; x++)
{
for (int y = 0; y < Figuur.Height; y++)
{
//double a = (double)(x - (Figuur.Width / 2)) / (double)(Figuur.Width / 4);
//double b = (double)(y - (Figuur.Height / 2)) / (double)(Figuur.Height / 4);
double a = (double)(x - (Figuur.Width / 2)) * (double)(schaal);
double b = (double)(y - (Figuur.Height /2)) * (double)(schaal);
Mandelgetal getal = new Mandelgetal(a, b);
Mandelgetal waarde = new Mandelgetal(0, 0);
int i = 0;
while (i < max)
{
//i++; //v1
waarde.Vermenigvuldig();
waarde.Toevoegen(getal);
if (waarde.Wortel() > 2.0)
{
break;
}
else
{
if (i % 2.0 == 0.0)
{
bitmap.SetPixel(x, y, Color.White);
i++; //v2
}
else
{
bitmap.SetPixel(x, y, Color.Black);
i++; //v2
}
}
}
if((a*a + b*b) > 4)
{
bitmap.SetPixel(x, y, Color.Black);
}
Figuur.Image = bitmap;
}
}
}
private void Button1_MouseClick(object sender, MouseEventArgs e)
{
Lees();
Genereer();
this.Invalidate();
}
}
}

How to recreate MenuItem ItemTapped and Command with a Button

I have a model called "ListItem", and I'm printing a list of them. I used to just use a ListView, but I wanted to print them horizontally, so I created a custom WrapLayout.
The ListView had simple ways to perform actions when the items were tapped or long-pressed. When a MenuItem was tapped, PutBack was called, and the ListItem was passed through as parameters. When a MenuItem was long-pressed, a button reading "delete" appeared in the toolbar, and when that was tapped, DeleteListItem was called and the ListItem was passed through as parameters.
Old Xaml:
<ListView x:Name="inactiveList" ItemTapped="PutBack" >
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Name}">
<TextCell.ContextActions>
<MenuItem Command="{Binding Source={x:Reference ListPage}, Path=DeleteListItem}"
CommandParameter="{Binding .}" Text="delete" />
</TextCell.ContextActions>
</TextCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
I'm having trouble recreating this functionality with the buttons generated by my custom WrapLayout. Is this possible to do? I tried adding a Clicked property to the buttons, but VS tells me I can only set Clicked with += or -=, which doesn't make any sense to me.
New Xaml:
<local:WrapLayout x:Name="inactiveList" Spacing="5" />
New Code-Behind:
namespace Myapp
{
public partial class ListPage
{
public Command DeleteListItem { get; set; }
public ListPage()
{
InitializeComponent();
ObservableCollection<ListItem> inactiveItems =
new ObservableCollection<ListItem>(
App.ListItemRepo.GetInactiveListItems());
inactiveList.ItemsSource = inactiveItems;
DeleteListItem = new Command(async (parameter) => {
ListItem item = (ListItem)parameter as ListItem;
App.ListItemRepo.DeleteListItemAsync(item);
ObservableCollection<ListItem> deleteInactiveItems =
new ObservableCollection<ListItem>(
await App.ListItemRepo.GetInactiveListItemsAsync());
inactiveList.ItemsSource = deleteInactiveItems;
});
}
public async void PutBack(object sender, ItemTappedEventArgs e)
{
var selectedListItem = e.Item as ListItem;
await App.ListItemRepo.SetListItemActive(selectedListItem);
ObservableCollection<ListItem> inactiveItems =
new ObservableCollection<ListItem>(
await App.ListItemRepo.GetInactiveListItemsAsync());
inactiveList.ItemsSource = inactiveItems;
}
}
public class WrapLayout : Layout<View>
{
public ObservableCollection<ListItem> ItemsSource
{
get { return (ObservableCollection<ListItem>)GetValue(ItemSourceProperty); }
set { SetValue(ItemSourceProperty, value); }
}
public static readonly BindableProperty ItemSourceProperty =
BindableProperty.Create
(
"ItemsSource",
typeof(ObservableCollection<ListItem>),
typeof(WrapLayout),
propertyChanged: (bindable, oldvalue, newvalue) => ((WrapLayout)bindable).AddViews()
);
// THIS IS WHERE THE BUTTONS ARE SET
void AddViews()
{
Children.Clear();
foreach (ListItem s in ItemsSource)
{
Button button = new Button();
button.Text = s.Name;
Children.Add(button);
}
}
public static readonly BindableProperty SpacingProperty =
BindableProperty.Create
(
"Spacing",
typeof(double),
typeof(WrapLayout),
10.0,
propertyChanged: (bindable, oldvalue, newvalue) => ((WrapLayout)bindable).OnSizeChanged()
);
public double Spacing
{
get { return (double)GetValue(SpacingProperty); }
set { SetValue(SpacingProperty, value); }
}
private void OnSizeChanged()
{
this.ForceLayout();
}
protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
{
if (WidthRequest > 0)
widthConstraint = Math.Min(widthConstraint, WidthRequest);
if (HeightRequest > 0)
heightConstraint = Math.Min(heightConstraint, HeightRequest);
double internalWidth = double.IsPositiveInfinity(widthConstraint) ? double.PositiveInfinity : Math.Max(0, widthConstraint);
double internalHeight = double.IsPositiveInfinity(heightConstraint) ? double.PositiveInfinity : Math.Max(0, heightConstraint);
return DoHorizontalMeasure(internalWidth, internalHeight);
}
private SizeRequest DoHorizontalMeasure(double widthConstraint, double heightConstraint)
{
int rowCount = 1;
double width = 0;
double height = 0;
double minWidth = 0;
double minHeight = 0;
double widthUsed = 0;
foreach (var item in Children)
{
var size = item.Measure(widthConstraint, heightConstraint);
height = Math.Max(height, size.Request.Height);
var newWidth = width + size.Request.Width + Spacing;
if (newWidth > widthConstraint)
{
rowCount++;
widthUsed = Math.Max(width, widthUsed);
width = size.Request.Width;
}
else
width = newWidth;
minHeight = Math.Max(minHeight, size.Minimum.Height);
minWidth = Math.Max(minWidth, size.Minimum.Width);
}
if (rowCount > 1)
{
width = Math.Max(width, widthUsed);
height = (height + Spacing) * rowCount - Spacing; // via MitchMilam
}
return new SizeRequest(new Size(width, height), new Size(minWidth, minHeight));
}
protected override void LayoutChildren(double x, double y, double width, double height)
{
double rowHeight = 0;
double yPos = y, xPos = x;
foreach (var child in Children.Where(c => c.IsVisible))
{
var request = child.Measure(width, height);
double childWidth = request.Request.Width;
double childHeight = request.Request.Height;
rowHeight = Math.Max(rowHeight, childHeight);
if (xPos + childWidth > width)
{
xPos = x;
yPos += rowHeight + Spacing;
rowHeight = 0;
}
var region = new Rectangle(xPos, yPos, childWidth, childHeight);
LayoutChildIntoBoundingRegion(child, region);
xPos += region.Width + Spacing;
}
}
}
}

Selection of multiple items

I'm working with RibbonControlsLibrary, WPF and VS2015, I need to perform an action on a selection of multi RibbonGallery. Following the images to facilitate understanding.
What I've done
As I want you to
Follows the code as was done:
<r:RibbonWindow.Resources>
<DataTemplate
x:Key="tableRectTemplate">
<DockPanel
Margin="-2,-1,-2,-1">
<Rectangle
Width="14"
Height="14"
Stroke="Gray"
ToolTip="{Binding}"
MouseEnter="Rectangle_MouseEnter">
</Rectangle>
</DockPanel>
</DataTemplate>
<x:Array
Type="sys:String"
x:Key="tablePickerRowColumn">
...items..
...items..
...items..
</x:Array>
</r:RibbonWindow.Resources>
<r:RibbonGallery
Name="_rgInsertTable"
ScrollViewer.VerticalScrollBarVisibility="Hidden"
Command="{StaticResource InserirTabelaHandler}"
MouseLeave="_rgInsertTable_MouseLeave">
<r:RibbonGalleryCategory
Name="_rgcInsertTable"
Header="Inserir tabela..."
MinColumnCount="10"
MaxColumnCount="10"
ItemTemplate="{StaticResource tableRectTemplate}"
ItemsSource="{Binding Source={StaticResource tablePickerRowColumn}, Path=SyncRoot}">
</r:RibbonGalleryCategory>
<r:RibbonSeparator/>
<r:RibbonMenuItem
Header="Inserir tabela"
ImageSource="/Images/Small_32bit/table.png"
Command="{StaticResource InserirTabelaHandler}"
MouseEnter="RibbonMenuItem_MouseEnter"/>
</r:RibbonGallery>
You should use a custom control,
Demo:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<local:MyTableControl x:Name="MyTableControl"></local:MyTableControl>
</Grid>
</Window>
Control (XAML):
<UserControl x:Class="WpfApplication1.MyTableControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UniformGrid x:Name="root">
</UniformGrid>
</UserControl>
Control (code):
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
namespace WpfApplication1
{
public partial class MyTableControl
{
private const int MinColumns = 1;
private const int MaxColumns = 10;
private const int MinRows = 1;
private const int MaxRows = 10;
public static readonly DependencyProperty RowsProperty = DependencyProperty.Register(
"Rows", typeof (int), typeof (MyTableControl), new PropertyMetadata(MinRows, OnRowsChanged, OnRowsCoerce));
public static readonly DependencyProperty ColumnsProperty = DependencyProperty.Register(
"Columns", typeof (int), typeof (MyTableControl),
new PropertyMetadata(MinColumns, OnColumnsChanged, OnColumnsCoerce));
private bool _pressed;
public MyTableControl()
{
InitializeComponent();
Columns = 5;
Rows = 5;
}
public int Rows
{
get { return (int) GetValue(RowsProperty); }
set { SetValue(RowsProperty, value); }
}
public int Columns
{
get { return (int) GetValue(ColumnsProperty); }
set { SetValue(ColumnsProperty, value); }
}
private static object OnRowsCoerce(DependencyObject d, object basevalue)
{
var i = (int) basevalue;
return i < MinRows ? MinRows : i > MaxRows ? MaxRows : i;
}
private static object OnColumnsCoerce(DependencyObject d, object basevalue)
{
var i = (int) basevalue;
return i < MinColumns ? MinColumns : i > MaxColumns ? MaxColumns : i;
}
private static void OnRowsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control1 = (MyTableControl) d;
control1.Populate();
}
private static void OnColumnsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control1 = (MyTableControl) d;
control1.Populate();
}
private void Populate()
{
root.Children.Clear();
root.Columns = Columns;
root.Rows = Rows;
for (var y = 0; y < Rows; y++)
{
for (var x = 0; x < Columns; x++)
{
var toggleButton = new ToggleButton {Tag = new Point(x, y)};
toggleButton.MouseEnter += ToggleButton_MouseEnter;
toggleButton.Click += ToggleButton_Click;
root.Children.Add(toggleButton);
}
}
}
private void ToggleButton_Click(object sender, RoutedEventArgs e)
{
_pressed = true; // stops selection
// little bug here, button will be unchecked since it is a toggle button
// but since you'll use images instead, this behavior will vanish
}
private void ToggleButton_MouseEnter(object sender, MouseEventArgs e)
{
if (_pressed)
{
return;
}
var button = (ToggleButton) sender;
var point = (Point) button.Tag;
var x = (int) point.X;
var y = (int) point.Y;
for (var i = 0; i < Columns*Rows; i++)
{
var element = (ToggleButton) root.Children[i];
var tag = (Point) element.Tag;
var x1 = (int) tag.X;
var y1 = (int) tag.Y;
element.IsChecked = x1 <= x && y1 <= y;
}
}
}
}
I used a ToggleButton for simplicity, you could further improve by using VisualTreeHelper instead of MouseEnter, etc ...
It might not be the most elegant solution, but I solved the problem this way.
XAML:
<r:RibbonMenuButton
LargeImageSource="/Images/Large_32bit/table.png"
Label="Tabela"
ToolTip="Tabela"
ToolTipDescription="Insere uma tabela no documento">
<r:RibbonGallery
ScrollViewer.VerticalScrollBarVisibility="Auto">
<r:RibbonGalleryCategory
Name="_rgcInsertTable"
Header="Inserir tabela">
<Canvas
Name="InsertTblCellMnuItemContainer"
Background="AliceBlue"
MouseMove="InsertTblCellMnuItemContainer_MouseMove"
MouseLeave="InsertTblCellMnuItemContainer_MouseLeave"/>
</r:RibbonGalleryCategory>
<r:RibbonSeparator/>
<r:RibbonMenuItem
Header="Inserir tabela"
ImageSource="/Images/Small_32bit/table.png"
Command="{StaticResource InserirTabelaHandler}"/>
</r:RibbonGallery>
</r:RibbonMenuButton>
Code:
public MainWindow()
{
InitializeComponent();
PopulateTableInsertMenuItem(10, 8, 15, 15, 2);
}
private void ResetTableCellRectangles()
{
foreach (Rectangle r in InsertTblCellMnuItemContainer.Children)
{
r.Fill = Brushes.AliceBlue;
r.Stroke = Brushes.Black;
}
}
private void InsertTblCellMnuItemContainer_MouseMove(object sender, MouseEventArgs e)
{
Point pos = e.GetPosition(InsertTblCellMnuItemContainer);
foreach (Rectangle r in InsertTblCellMnuItemContainer.Children)
{
Vector posRect = VisualTreeHelper.GetOffset(r);
if ((posRect.X <= pos.X) && (posRect.Y <= pos.Y))
{
r.Fill = Brushes.LightYellow;
r.Stroke = Brushes.Orange;
}
else
{
r.Fill = Brushes.AliceBlue;
r.Stroke = Brushes.Black;
}
}
}
private void InsertTblCellMnuItemContainer_MouseLeave(object sender, MouseEventArgs e)
{
ResetTableCellRectangles();
_rgcInsertTable.Header = "Inserir tabela";
}
private void PopulateTableInsertMenuItem(int width, int height, int rectWidth, int rectHeight, int margin)
{
Rectangle r;
InsertTblCellMnuItemContainer.Width = (rectWidth + margin) * width;
InsertTblCellMnuItemContainer.Height = (rectHeight + margin) * height;
for (int j = 0; j < height; ++j)
{
for (int i = 0; i < width; ++i)
{
// Create new rectangle
r = new Rectangle
{
Width = rectWidth,
Height = rectHeight,
Stroke = Brushes.Black,
Fill = Brushes.AliceBlue,
Tag = new Point(i + 1, j + 1), // Remember rectangle's position in grid somehow
};
r.MouseLeftButtonDown += new MouseButtonEventHandler(TableInsertRectangle_MouseLeftButtonDown);
r.MouseEnter += new MouseEventHandler(TableInsertRectangle_MouseEnter);
// Set position in canvas
Canvas.SetLeft(r, (i * margin) + (i * rectWidth));
Canvas.SetTop(r, (j * margin) + (j * rectHeight));
// Add rectangle to canvas
InsertTblCellMnuItemContainer.Children.Add(r);
}
}
}
void TableInsertRectangle_MouseEnter(object sender, MouseEventArgs e)
{
Point rectKoords = (Point)((Rectangle)sender).Tag;
_rgcInsertTable.Header = rectKoords.X.ToString() + "x" + rectKoords.Y.ToString() + " tabela";
}
void TableInsertRectangle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Point rectKoords = (Point)((Rectangle)sender).Tag;
_textControl.Tables.Add((int)rectKoords.Y, (int)rectKoords.X);
}
Any improvement is welcome.

How to create a usercontrol on different thread?

I am creating a UI that will be displaying generated patterns similar to fractals and cellular automation, but they will be continuously generating and automated.
The pixels and pixel colors will be displayed as a grid of squares in a usercontrol. I've already created the usercontrol to display this but since it is constantly calculation at every timer.tick it dramatically slows down the rest of the UI and causes all other elements to stutter.
So I looked into threading and set the "calculating" part in a BackgroundWorker DoWork(), which ultimately didn't end up working out the way I wanted it to. The BackgroundWorker is using data from the main thread (Rectangle[400]), so I had to use Dispatcher.Invoke(new Action(() => { })); which simply defeats the purpose, the program still ran very "stuttery".
So, how can I create a usercontrol...name:display_control entirely on a separate thread and have it appear in another usercontrol...name:unsercontrol1 (Which is running in the main thread), ? Then I could possibly databind the user_input data with the usercontrol1 User_Input_Class instance.
Or, is there a better way to achieve this? Only other way I can think of doing this is to simply create an entirely separate program for the display and share a file containing the user_input data which is very unprofessional.
public partial class Fractal_Gen_A : UserControl
{
byte W_R = 0;
byte W_G = 255;
byte W_B = 0;
int Pixel_Max_Width = 20;
int Pixel_Max_Height = 20;
Color[] Pixel_Color = new Color[20 * 20]; //Width_Max * Canvas_Height_Count
Rectangle[] Pixel = new Rectangle[20 * 20];
Color[] Temp_Color = new Color[20 * 20];
BackgroundWorker worker = new BackgroundWorker();
private void Timer_Tick(object sender, EventArgs e)
{
try { worker.RunWorkerAsync(); }
catch {}
}
Function_Classes.Main_Binder.FGA LB = new Function_Classes.Main_Binder.FGA(); //LB = local Binder
DispatcherTimer Timer = new DispatcherTimer();
public Fractal_Gen_A()
{
LB.Brush = new SolidColorBrush[Pixel_Max_Width * Pixel_Max_Height];
InitializeComponent();
DataContext = LB;
Set_Up_Binded_Brushes();
worker.DoWork += Worker_Work;
Timer.Tick += new EventHandler(Timer_Tick);
Timer.Interval = new TimeSpan(0, 0, 0, 0, 300);
}
private void Set_Up_Binded_Brushes()
{
for (int i = 0; i < Pixel_Max_Width *Pixel_Max_Height; i++)
{
LB.Brush[i] = new SolidColorBrush(Color.FromRgb(255, 0, 0));
}
}
private delegate void UpdateMyDelegatedelegate(int i);
private void UpdateMyDelegateLabel(int i){}
private void Worker_Work(object sender, DoWorkEventArgs e)
{
try
{
BackgroundWorker Worker = sender as BackgroundWorker;
SolidColorBrush[] Temp_Brush = new SolidColorBrush[Pixel_Max_Height * Pixel_Max_Width];
for (int h = 0; h < Pixel_Max_Height - 1; h++)
{
for (int w = 0; w < Pixel_Max_Width; w++)
{
Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal, new Action(() => {
Temp_Brush[((h + 1) * Pixel_Max_Width) + w] = new SolidColorBrush();
Temp_Brush[((h + 1) * Pixel_Max_Width) + w].Color = LB.Brush[(h * Pixel_Max_Width) + w].Color;
}));
}
}
W_R += 23;
for (int w = 0; w < Pixel_Max_Width; w++)
{
W_B += 23 % 255;
W_R += 23 % 255;
Temp_Brush[w].Color = Color.FromRgb(W_R, W_B, W_G);
}
Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal, new Action(() => {
Array.Copy(Temp_Brush, 0, LB.Brush, 0, Pixel_Max_Height * Pixel_Max_Width);
}));
UpdateMyDelegatedelegate UpdateMyDelegate = new UpdateMyDelegatedelegate(UpdateMyDelegateLabel);
}
catch { }
}
private void Worker_Done(object sender, RunWorkerCompletedEventArgs e)
{
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
double X_Set = Pixel_Canvas.ActualWidth / Pixel_Max_Width;
double Y_Set = Pixel_Canvas.ActualHeight / Pixel_Max_Height;
for (int h = 0; h < Pixel_Max_Height; h++)
{
for (int w = 0; w < Pixel_Max_Width; w++)
{
Pixel_Color[(h * Pixel_Max_Width) + w] = Color.FromRgb(255, 0, 0);
Pixel[(h * Pixel_Max_Width) + w] = new Rectangle();
Pixel[(h * Pixel_Max_Width) + w].Width = Pixel_Canvas.ActualWidth / Pixel_Max_Width;
Pixel[(h * Pixel_Max_Width) + w].Height = Pixel_Canvas.ActualHeight / Pixel_Max_Height;
Pixel[(h * Pixel_Max_Width) + w].Stroke = new SolidColorBrush(Color.FromRgb(100, 100, 100)); //Pixel_Color[(h * Pixel_Max_Width) + w]
Pixel[(h * Pixel_Max_Width) + w].StrokeThickness = .5;
Pixel[(h * Pixel_Max_Width) + w].Fill = new SolidColorBrush(Color.FromRgb(255, 0, 0)); //Pixel_Color[(h * Pixel_Max_Width) + w]
Canvas.SetLeft(Pixel[(h * Pixel_Max_Width) + w], (X_Set * w) + 0);
Canvas.SetTop(Pixel[(h * Pixel_Max_Height) + w], (Y_Set * h) + 0);
Pixel_Canvas.Children.Add(Pixel[(h * Pixel_Max_Height) + w]);
int Temp_Count = (h * Pixel_Max_Width) + w;
string Temp_Bind = "Brush[" + Temp_Count.ToString() + "]";
Binding Bind = new Binding(Temp_Bind);
Pixel[(h * Pixel_Max_Height) + w].SetBinding(Rectangle.FillProperty, Bind );
// Dispatcher.Invoke(new Action(() => { }));
Timer.Start();
}
}
Timer.Start();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Window pw = new PopUp_Window();
pw.Show();
}
}
Basically, I am using usercontrols to act as views, 2 will be displayed at all times, one on the left one on the right.
Ok. Delete all your code and start all over.
If you're working with WPF, you really need to embrace The WPF Mentality
This is how you do that in WPF:
<Window x:Class="MiscSamples.Fractals"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Fractals" Height="300" Width="300">
<ItemsControl ItemsSource="{Binding Cells}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="{Binding Size}" Columns="{Binding Size}"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="Gray" BorderThickness="2">
<Border.Background>
<SolidColorBrush Color="{Binding Color,Mode=OneTime}"/>
</Border.Background>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>
Code Behind:
public partial class Fractals : Window
{
public Fractals()
{
InitializeComponent();
DataContext = new FractalViewModel();
}
}
ViewModel:
public class FractalViewModel:PropertyChangedBase
{
private ObservableCollection<FractalCell> _cells;
public int Rows { get; set; }
public int Columns { get; set; }
public ObservableCollection<FractalCell> Cells
{
get { return _cells; }
set
{
_cells = value;
OnPropertyChanged("Cells");
}
}
public FractalViewModel()
{
var ctx = TaskScheduler.FromCurrentSynchronizationContext();
Task.Factory.StartNew(() => CreateFractalCellsAsync())
.ContinueWith(x => Cells = new ObservableCollection<FractalCell>(x.Result), ctx);
}
private List<FractalCell> CreateFractalCellsAsync()
{
var cells = new List<FractalCell>();
var colors = typeof(Colors).GetProperties().Select(x => (Color)x.GetValue(null, null)).ToList();
var random = new Random();
for (int i = 0; i < 32; i++)
{
for (int j = 0; j < 32; j++)
{
cells.Add(new FractalCell() { Row = i, Column = j, Color = colors[random.Next(0, colors.Count)] });
}
}
return cells;
}
}
Data Item:
public class FractalCell:PropertyChangedBase
{
public int Row { get; set; }
public int Column { get; set; }
public Color Color { get; set; }
}
PropertyChangedBase class:
public class PropertyChangedBase:INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Result:
Notice how I'm not manipulating any UI elements in procedural code. Everything is done with simple, simple Properties and INotifyPropertyChanged. That's how you program in WPF.
I'm using an ItemsControl with a UniformGrid and a simple DataTemplate for the cells.
The example is generating random colors, but you can get this from whatever data source you like. Notice that the data is completely decoupled from the UI and thus it makes it much easier for you to manipulate your own, simple classes rather than the complex and arcane WPF object model.
It also makes it easier for you to implement a multi-threaded scenario because, again, you are not dealing with the UI, but rather with Data. Remember that the UI can only be manipulated in the UI Thread.
WPF Rocks. Just copy and paste my code in a File -> New Project -> WPF Application and see the results for yourself.
Let me know if you need further help.

Pan & Zoom Image

I want to create a simple image viewer in WPF that will enable the user to:
Pan (by mouse dragging the image).
Zoom (with a slider).
Show overlays (rectangle selection for example).
Show original image (with scroll bars if needed).
Can you explain how to do it?
I didn't find a good sample on the web.
Should I use ViewBox? Or ImageBrush?
Do I need ScrollViewer?
After using samples from this question I've made complete version of pan & zoom app with proper zooming relative to mouse pointer. All pan & zoom code has been moved to separate class called ZoomBorder.
ZoomBorder.cs
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace PanAndZoom
{
public class ZoomBorder : Border
{
private UIElement child = null;
private Point origin;
private Point start;
private TranslateTransform GetTranslateTransform(UIElement element)
{
return (TranslateTransform)((TransformGroup)element.RenderTransform)
.Children.First(tr => tr is TranslateTransform);
}
private ScaleTransform GetScaleTransform(UIElement element)
{
return (ScaleTransform)((TransformGroup)element.RenderTransform)
.Children.First(tr => tr is ScaleTransform);
}
public override UIElement Child
{
get { return base.Child; }
set
{
if (value != null && value != this.Child)
this.Initialize(value);
base.Child = value;
}
}
public void Initialize(UIElement element)
{
this.child = element;
if (child != null)
{
TransformGroup group = new TransformGroup();
ScaleTransform st = new ScaleTransform();
group.Children.Add(st);
TranslateTransform tt = new TranslateTransform();
group.Children.Add(tt);
child.RenderTransform = group;
child.RenderTransformOrigin = new Point(0.0, 0.0);
this.MouseWheel += child_MouseWheel;
this.MouseLeftButtonDown += child_MouseLeftButtonDown;
this.MouseLeftButtonUp += child_MouseLeftButtonUp;
this.MouseMove += child_MouseMove;
this.PreviewMouseRightButtonDown += new MouseButtonEventHandler(
child_PreviewMouseRightButtonDown);
}
}
public void Reset()
{
if (child != null)
{
// reset zoom
var st = GetScaleTransform(child);
st.ScaleX = 1.0;
st.ScaleY = 1.0;
// reset pan
var tt = GetTranslateTransform(child);
tt.X = 0.0;
tt.Y = 0.0;
}
}
#region Child Events
private void child_MouseWheel(object sender, MouseWheelEventArgs e)
{
if (child != null)
{
var st = GetScaleTransform(child);
var tt = GetTranslateTransform(child);
double zoom = e.Delta > 0 ? .2 : -.2;
if (!(e.Delta > 0) && (st.ScaleX < .4 || st.ScaleY < .4))
return;
Point relative = e.GetPosition(child);
double absoluteX;
double absoluteY;
absoluteX = relative.X * st.ScaleX + tt.X;
absoluteY = relative.Y * st.ScaleY + tt.Y;
st.ScaleX += zoom;
st.ScaleY += zoom;
tt.X = absoluteX - relative.X * st.ScaleX;
tt.Y = absoluteY - relative.Y * st.ScaleY;
}
}
private void child_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (child != null)
{
var tt = GetTranslateTransform(child);
start = e.GetPosition(this);
origin = new Point(tt.X, tt.Y);
this.Cursor = Cursors.Hand;
child.CaptureMouse();
}
}
private void child_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (child != null)
{
child.ReleaseMouseCapture();
this.Cursor = Cursors.Arrow;
}
}
void child_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
this.Reset();
}
private void child_MouseMove(object sender, MouseEventArgs e)
{
if (child != null)
{
if (child.IsMouseCaptured)
{
var tt = GetTranslateTransform(child);
Vector v = start - e.GetPosition(this);
tt.X = origin.X - v.X;
tt.Y = origin.Y - v.Y;
}
}
}
#endregion
}
}
MainWindow.xaml
<Window x:Class="PanAndZoom.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PanAndZoom"
Title="PanAndZoom" Height="600" Width="900" WindowStartupLocation="CenterScreen">
<Grid>
<local:ZoomBorder x:Name="border" ClipToBounds="True" Background="Gray">
<Image Source="image.jpg"/>
</local:ZoomBorder>
</Grid>
</Window>
MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace PanAndZoom
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}
The way I solved this problem was to place the image within a Border with it's ClipToBounds property set to True. The RenderTransformOrigin on the image is then set to 0.5,0.5 so the image will start zooming on the center of the image. The RenderTransform is also set to a TransformGroup containing a ScaleTransform and a TranslateTransform.
I then handled the MouseWheel event on the image to implement zooming
private void image_MouseWheel(object sender, MouseWheelEventArgs e)
{
var st = (ScaleTransform)image.RenderTransform;
double zoom = e.Delta > 0 ? .2 : -.2;
st.ScaleX += zoom;
st.ScaleY += zoom;
}
To handle the panning the first thing I did was to handle the MouseLeftButtonDown event on the image, to capture the mouse and to record it's location, I also store the current value of the TranslateTransform, this what is updated to implement panning.
Point start;
Point origin;
private void image_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
image.CaptureMouse();
var tt = (TranslateTransform)((TransformGroup)image.RenderTransform)
.Children.First(tr => tr is TranslateTransform);
start = e.GetPosition(border);
origin = new Point(tt.X, tt.Y);
}
Then I handled the MouseMove event to update the TranslateTransform.
private void image_MouseMove(object sender, MouseEventArgs e)
{
if (image.IsMouseCaptured)
{
var tt = (TranslateTransform)((TransformGroup)image.RenderTransform)
.Children.First(tr => tr is TranslateTransform);
Vector v = start - e.GetPosition(border);
tt.X = origin.X - v.X;
tt.Y = origin.Y - v.Y;
}
}
Finally don't forget to release the mouse capture.
private void image_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
image.ReleaseMouseCapture();
}
As for the selection handles for resizing this can be accomplished using an adorner, check out this article for more information.
The answer was posted above but wasn't complete. here is the completed version:
XAML
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MapTest.Window1"
x:Name="Window"
Title="Window1"
Width="1950" Height="1546" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:Controls="clr-namespace:WPFExtensions.Controls;assembly=WPFExtensions" mc:Ignorable="d" Background="#FF000000">
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition Height="52.92"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border Grid.Row="1" Name="border">
<Image Name="image" Source="map3-2.png" Opacity="1" RenderTransformOrigin="0.5,0.5" />
</Border>
</Grid>
Code Behind
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace MapTest
{
public partial class Window1 : Window
{
private Point origin;
private Point start;
public Window1()
{
InitializeComponent();
TransformGroup group = new TransformGroup();
ScaleTransform xform = new ScaleTransform();
group.Children.Add(xform);
TranslateTransform tt = new TranslateTransform();
group.Children.Add(tt);
image.RenderTransform = group;
image.MouseWheel += image_MouseWheel;
image.MouseLeftButtonDown += image_MouseLeftButtonDown;
image.MouseLeftButtonUp += image_MouseLeftButtonUp;
image.MouseMove += image_MouseMove;
}
private void image_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
image.ReleaseMouseCapture();
}
private void image_MouseMove(object sender, MouseEventArgs e)
{
if (!image.IsMouseCaptured) return;
var tt = (TranslateTransform) ((TransformGroup) image.RenderTransform).Children.First(tr => tr is TranslateTransform);
Vector v = start - e.GetPosition(border);
tt.X = origin.X - v.X;
tt.Y = origin.Y - v.Y;
}
private void image_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
image.CaptureMouse();
var tt = (TranslateTransform) ((TransformGroup) image.RenderTransform).Children.First(tr => tr is TranslateTransform);
start = e.GetPosition(border);
origin = new Point(tt.X, tt.Y);
}
private void image_MouseWheel(object sender, MouseWheelEventArgs e)
{
TransformGroup transformGroup = (TransformGroup) image.RenderTransform;
ScaleTransform transform = (ScaleTransform) transformGroup.Children[0];
double zoom = e.Delta > 0 ? .2 : -.2;
transform.ScaleX += zoom;
transform.ScaleY += zoom;
}
}
}
I have some source code demonstrating this Jot the sticky note app.
Pan: Put the image inside of a Canvas. Implement Mouse Up, Down, and Move events to move the Canvas.Top, Canvas.Left properties. When down, you mark a isDraggingFlag to true, when up you set the flag to false. On move, you check if the flag is set, if it is you offset the Canvas.Top and Canvas.Left properties on the image within the canvas.
Zoom: Bind the slider to the Scale Transform of the Canvas
Show overlays: add additional canvas's with no background ontop the canvas containing the image.
show original image: image control inside of a ViewBox
Try this Zoom Control: http://wpfextensions.codeplex.com
usage of the control is very simple, reference to the wpfextensions assembly than:
<wpfext:ZoomControl>
<Image Source="..."/>
</wpfext:ZoomControl>
Scrollbars not supported at this moment. (It will be in the next release which will be available in one or two week).
#Anothen and #Number8 - The Vector class is not available in Silverlight, so to make it work we just need to keep a record of the last position sighted the last time the MouseMove event was called, and compare the two points to find the difference; then adjust the transform.
XAML:
<Border Name="viewboxBackground" Background="Black">
<Viewbox Name="viewboxMain">
<!--contents go here-->
</Viewbox>
</Border>
Code-behind:
public Point _mouseClickPos;
public bool bMoving;
public MainPage()
{
InitializeComponent();
viewboxMain.RenderTransform = new CompositeTransform();
}
void MouseMoveHandler(object sender, MouseEventArgs e)
{
if (bMoving)
{
//get current transform
CompositeTransform transform = viewboxMain.RenderTransform as CompositeTransform;
Point currentPos = e.GetPosition(viewboxBackground);
transform.TranslateX += (currentPos.X - _mouseClickPos.X) ;
transform.TranslateY += (currentPos.Y - _mouseClickPos.Y) ;
viewboxMain.RenderTransform = transform;
_mouseClickPos = currentPos;
}
}
void MouseClickHandler(object sender, MouseButtonEventArgs e)
{
_mouseClickPos = e.GetPosition(viewboxBackground);
bMoving = true;
}
void MouseReleaseHandler(object sender, MouseButtonEventArgs e)
{
bMoving = false;
}
Also note that you don't need a TransformGroup or collection to implement pan and zoom; instead, a CompositeTransform will do the trick with less hassle.
I'm pretty sure this is really inefficient in terms of resource usage, but at least it works :)
To zoom relative to the mouse position, all you need is:
var position = e.GetPosition(image1);
image1.RenderTransformOrigin = new Point(position.X / image1.ActualWidth, position.Y / image1.ActualHeight);
I also tried this answer but was not entirely happy with the result. I kept googling around and finally found a Nuget Package that helped me to manage the result I wanted, anno 2021. I would like to share it with the former developers of Stack Overflow.
I used this Nuget Package Gu.WPF.Geometry found via this Github Repository. All credits for develoment should go to Johan Larsson, the owner of this package.
How I used it? I wanted to have the commands as buttons below the zoombox, as shown here in MachineLayoutControl.xaml .
<UserControl
x:Class="MyLib.MachineLayoutControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:csmachinelayoutdrawlib="clr-namespace:CSMachineLayoutDrawLib"
xmlns:effects="http://gu.se/Geometry">
<UserControl.Resources>
<ResourceDictionary Source="Resources/ResourceDictionaries/AllResourceDictionariesCombined.xaml" />
</UserControl.Resources>
<Grid Margin="0">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border
Grid.Row="0"
Margin="0,0"
Padding="0"
BorderThickness="1"
Style="{StaticResource Border_Head}"
Visibility="Visible">
<effects:Zoombox
x:Name="ImageBox"
IsManipulationEnabled="True"
MaxZoom="10"
MinZoom="0.1"
Visibility="{Binding Zoombox_Visibility}">
<ContentControl Content="{Binding Viewing_Canvas}" />
</effects:Zoombox>
</Border>
<StackPanel
Grid.Column="1"
Margin="10"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Command="effects:ZoomCommands.Increase"
CommandParameter="2.0"
CommandTarget="{Binding ElementName=ImageBox}"
Content="Zoom In"
Style="{StaticResource StyleForResizeButtons}" />
<Button
Command="effects:ZoomCommands.Decrease"
CommandParameter="2.0"
CommandTarget="{Binding ElementName=ImageBox}"
Content="Zoom Out"
Style="{StaticResource StyleForResizeButtons}" />
<Button
Command="effects:ZoomCommands.Uniform"
CommandTarget="{Binding ElementName=ImageBox}"
Content="See Full Machine"
Style="{StaticResource StyleForResizeButtons}" />
<Button
Command="effects:ZoomCommands.UniformToFill"
CommandTarget="{Binding ElementName=ImageBox}"
Content="Zoom To Machine Width"
Style="{StaticResource StyleForResizeButtons}" />
</StackPanel>
</Grid>
</UserControl>
In the underlying Viewmodel, I had the following relevant code:
public Visibility Zoombox_Visibility { get => movZoombox_Visibility; set { movZoombox_Visibility = value; OnPropertyChanged(nameof(Zoombox_Visibility)); } }
public Canvas Viewing_Canvas { get => mdvViewing_Canvas; private set => mdvViewing_Canvas = value; }
Also, I wanted that immediately on loading, the Uniform to Fill Command was executed, this is something that I managed to do in the code-behind MachineLayoutControl.xaml.cs . You see that I only set the Zoombox to visible if the command is executed, to avoid "flickering" when the usercontrol is loading.
public partial class MachineLayoutControl : UserControl
{
#region Constructors
public MachineLayoutControl()
{
InitializeComponent();
Loaded += MyWindow_Loaded;
}
#endregion Constructors
#region EventHandlers
private void MyWindow_Loaded(object sender, RoutedEventArgs e)
{
Application.Current.Dispatcher.BeginInvoke(
DispatcherPriority.ApplicationIdle,
new Action(() =>
{
ZoomCommands.Uniform.Execute(null, ImageBox);
((MachineLayoutControlViewModel)DataContext).Zoombox_Visibility = Visibility.Visible;
}));
}
#endregion EventHandlers
}
# Merk
For ur solution insted of lambda expression you can use following code:
//var tt = (TranslateTransform)((TransformGroup)image.RenderTransform).Children.First(tr => tr is TranslateTransform);
TranslateTransform tt = null;
TransformGroup transformGroup = (TransformGroup)grid.RenderTransform;
for (int i = 0; i < transformGroup.Children.Count; i++)
{
if (transformGroup.Children[i] is TranslateTransform)
tt = (TranslateTransform)transformGroup.Children[i];
}
this code can be use as is for .Net Frame work 3.0 or 2.0
Hope It helps you :-)
Yet another version of the same kind of control. It has similar functionality as the others, but it adds:
Touch support (drag/pinch)
The image can be deleted (normally, the Image control locks the image on disk, so you cannot delete it).
An inner border child, so the panned image doesn't overlap the border. In case of borders with rounded rectangles, look for ClippedBorder classes.
Usage is simple:
<Controls:ImageViewControl ImagePath="{Binding ...}" />
And the code:
public class ImageViewControl : Border
{
private Point origin;
private Point start;
private Image image;
public ImageViewControl()
{
ClipToBounds = true;
Loaded += OnLoaded;
}
#region ImagePath
/// <summary>
/// ImagePath Dependency Property
/// </summary>
public static readonly DependencyProperty ImagePathProperty = DependencyProperty.Register("ImagePath", typeof (string), typeof (ImageViewControl), new FrameworkPropertyMetadata(string.Empty, OnImagePathChanged));
/// <summary>
/// Gets or sets the ImagePath property. This dependency property
/// indicates the path to the image file.
/// </summary>
public string ImagePath
{
get { return (string) GetValue(ImagePathProperty); }
set { SetValue(ImagePathProperty, value); }
}
/// <summary>
/// Handles changes to the ImagePath property.
/// </summary>
private static void OnImagePathChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var target = (ImageViewControl) d;
var oldImagePath = (string) e.OldValue;
var newImagePath = target.ImagePath;
target.ReloadImage(newImagePath);
target.OnImagePathChanged(oldImagePath, newImagePath);
}
/// <summary>
/// Provides derived classes an opportunity to handle changes to the ImagePath property.
/// </summary>
protected virtual void OnImagePathChanged(string oldImagePath, string newImagePath)
{
}
#endregion
private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
image = new Image {
//IsManipulationEnabled = true,
RenderTransformOrigin = new Point(0.5, 0.5),
RenderTransform = new TransformGroup {
Children = new TransformCollection {
new ScaleTransform(),
new TranslateTransform()
}
}
};
// NOTE I use a border as the first child, to which I add the image. I do this so the panned image doesn't partly obscure the control's border.
// In case you are going to use rounder corner's on this control, you may to update your clipping, as in this example:
// http://wpfspark.wordpress.com/2011/06/08/clipborder-a-wpf-border-that-clips/
var border = new Border {
IsManipulationEnabled = true,
ClipToBounds = true,
Child = image
};
Child = border;
image.MouseWheel += (s, e) =>
{
var zoom = e.Delta > 0
? .2
: -.2;
var position = e.GetPosition(image);
image.RenderTransformOrigin = new Point(position.X / image.ActualWidth, position.Y / image.ActualHeight);
var st = (ScaleTransform)((TransformGroup)image.RenderTransform).Children.First(tr => tr is ScaleTransform);
st.ScaleX += zoom;
st.ScaleY += zoom;
e.Handled = true;
};
image.MouseLeftButtonDown += (s, e) =>
{
if (e.ClickCount == 2)
ResetPanZoom();
else
{
image.CaptureMouse();
var tt = (TranslateTransform) ((TransformGroup) image.RenderTransform).Children.First(tr => tr is TranslateTransform);
start = e.GetPosition(this);
origin = new Point(tt.X, tt.Y);
}
e.Handled = true;
};
image.MouseMove += (s, e) =>
{
if (!image.IsMouseCaptured) return;
var tt = (TranslateTransform) ((TransformGroup) image.RenderTransform).Children.First(tr => tr is TranslateTransform);
var v = start - e.GetPosition(this);
tt.X = origin.X - v.X;
tt.Y = origin.Y - v.Y;
e.Handled = true;
};
image.MouseLeftButtonUp += (s, e) => image.ReleaseMouseCapture();
//NOTE I apply the manipulation to the border, and not to the image itself (which caused stability issues when translating)!
border.ManipulationDelta += (o, e) =>
{
var st = (ScaleTransform)((TransformGroup)image.RenderTransform).Children.First(tr => tr is ScaleTransform);
var tt = (TranslateTransform)((TransformGroup)image.RenderTransform).Children.First(tr => tr is TranslateTransform);
st.ScaleX *= e.DeltaManipulation.Scale.X;
st.ScaleY *= e.DeltaManipulation.Scale.X;
tt.X += e.DeltaManipulation.Translation.X;
tt.Y += e.DeltaManipulation.Translation.Y;
e.Handled = true;
};
}
private void ResetPanZoom()
{
var st = (ScaleTransform)((TransformGroup)image.RenderTransform).Children.First(tr => tr is ScaleTransform);
var tt = (TranslateTransform)((TransformGroup)image.RenderTransform).Children.First(tr => tr is TranslateTransform);
st.ScaleX = st.ScaleY = 1;
tt.X = tt.Y = 0;
image.RenderTransformOrigin = new Point(0.5, 0.5);
}
/// <summary>
/// Load the image (and do not keep a hold on it, so we can delete the image without problems)
/// </summary>
/// <see cref="http://blogs.vertigo.com/personal/ralph/Blog/Lists/Posts/Post.aspx?ID=18"/>
/// <param name="path"></param>
private void ReloadImage(string path)
{
try
{
ResetPanZoom();
// load the image, specify CacheOption so the file is not locked
var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.UriSource = new Uri(path, UriKind.RelativeOrAbsolute);
bitmapImage.EndInit();
image.Source = bitmapImage;
}
catch (SystemException e)
{
Console.WriteLine(e.Message);
}
}
}
This will zoom in and out as well as pan but keep the image within the bounds of the container. Written as a control so add the style to the App.xaml directly or through the Themes/Viewport.xaml.
For readability I've also uploaded this on gist and github
I've also packaged this up on nuget
PM > Install-Package Han.Wpf.ViewportControl
./Controls/Viewport.cs:
public class Viewport : ContentControl
{
private bool _capture;
private FrameworkElement _content;
private Matrix _matrix;
private Point _origin;
public static readonly DependencyProperty MaxZoomProperty =
DependencyProperty.Register(
nameof(MaxZoom),
typeof(double),
typeof(Viewport),
new PropertyMetadata(0d));
public static readonly DependencyProperty MinZoomProperty =
DependencyProperty.Register(
nameof(MinZoom),
typeof(double),
typeof(Viewport),
new PropertyMetadata(0d));
public static readonly DependencyProperty ZoomSpeedProperty =
DependencyProperty.Register(
nameof(ZoomSpeed),
typeof(float),
typeof(Viewport),
new PropertyMetadata(0f));
public static readonly DependencyProperty ZoomXProperty =
DependencyProperty.Register(
nameof(ZoomX),
typeof(double),
typeof(Viewport),
new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public static readonly DependencyProperty ZoomYProperty =
DependencyProperty.Register(
nameof(ZoomY),
typeof(double),
typeof(Viewport),
new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public static readonly DependencyProperty OffsetXProperty =
DependencyProperty.Register(
nameof(OffsetX),
typeof(double),
typeof(Viewport),
new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public static readonly DependencyProperty OffsetYProperty =
DependencyProperty.Register(
nameof(OffsetY),
typeof(double),
typeof(Viewport),
new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public static readonly DependencyProperty BoundsProperty =
DependencyProperty.Register(
nameof(Bounds),
typeof(Rect),
typeof(Viewport),
new FrameworkPropertyMetadata(default(Rect), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public Rect Bounds
{
get => (Rect) GetValue(BoundsProperty);
set => SetValue(BoundsProperty, value);
}
public double MaxZoom
{
get => (double) GetValue(MaxZoomProperty);
set => SetValue(MaxZoomProperty, value);
}
public double MinZoom
{
get => (double) GetValue(MinZoomProperty);
set => SetValue(MinZoomProperty, value);
}
public double OffsetX
{
get => (double) GetValue(OffsetXProperty);
set => SetValue(OffsetXProperty, value);
}
public double OffsetY
{
get => (double) GetValue(OffsetYProperty);
set => SetValue(OffsetYProperty, value);
}
public float ZoomSpeed
{
get => (float) GetValue(ZoomSpeedProperty);
set => SetValue(ZoomSpeedProperty, value);
}
public double ZoomX
{
get => (double) GetValue(ZoomXProperty);
set => SetValue(ZoomXProperty, value);
}
public double ZoomY
{
get => (double) GetValue(ZoomYProperty);
set => SetValue(ZoomYProperty, value);
}
public Viewport()
{
DefaultStyleKey = typeof(Viewport);
Loaded += OnLoaded;
Unloaded += OnUnloaded;
}
private void Arrange(Size desired, Size render)
{
_matrix = Matrix.Identity;
var zx = desired.Width / render.Width;
var zy = desired.Height / render.Height;
var cx = render.Width < desired.Width ? render.Width / 2.0 : 0.0;
var cy = render.Height < desired.Height ? render.Height / 2.0 : 0.0;
var zoom = Math.Min(zx, zy);
if (render.Width > desired.Width &&
render.Height > desired.Height)
{
cx = (desired.Width - (render.Width * zoom)) / 2.0;
cy = (desired.Height - (render.Height * zoom)) / 2.0;
_matrix = new Matrix(zoom, 0d, 0d, zoom, cx, cy);
}
else
{
_matrix.ScaleAt(zoom, zoom, cx, cy);
}
}
private void Attach(FrameworkElement content)
{
content.MouseMove += OnMouseMove;
content.MouseLeave += OnMouseLeave;
content.MouseWheel += OnMouseWheel;
content.MouseLeftButtonDown += OnMouseLeftButtonDown;
content.MouseLeftButtonUp += OnMouseLeftButtonUp;
content.SizeChanged += OnSizeChanged;
content.MouseRightButtonDown += OnMouseRightButtonDown;
}
private void ChangeContent(FrameworkElement content)
{
if (content != null && !Equals(content, _content))
{
if (_content != null)
{
Detatch();
}
Attach(content);
_content = content;
}
}
private double Constrain(double value, double min, double max)
{
if (min > max)
{
min = max;
}
if (value <= min)
{
return min;
}
if (value >= max)
{
return max;
}
return value;
}
private void Constrain()
{
var x = Constrain(_matrix.OffsetX, _content.ActualWidth - _content.ActualWidth * _matrix.M11, 0);
var y = Constrain(_matrix.OffsetY, _content.ActualHeight - _content.ActualHeight * _matrix.M22, 0);
_matrix = new Matrix(_matrix.M11, 0d, 0d, _matrix.M22, x, y);
}
private void Detatch()
{
_content.MouseMove -= OnMouseMove;
_content.MouseLeave -= OnMouseLeave;
_content.MouseWheel -= OnMouseWheel;
_content.MouseLeftButtonDown -= OnMouseLeftButtonDown;
_content.MouseLeftButtonUp -= OnMouseLeftButtonUp;
_content.SizeChanged -= OnSizeChanged;
_content.MouseRightButtonDown -= OnMouseRightButtonDown;
}
private void Invalidate()
{
if (_content != null)
{
Constrain();
_content.RenderTransformOrigin = new Point(0, 0);
_content.RenderTransform = new MatrixTransform(_matrix);
_content.InvalidateVisual();
ZoomX = _matrix.M11;
ZoomY = _matrix.M22;
OffsetX = _matrix.OffsetX;
OffsetY = _matrix.OffsetY;
var rect = new Rect
{
X = OffsetX * -1,
Y = OffsetY * -1,
Width = ActualWidth,
Height = ActualHeight
};
Bounds = rect;
}
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_matrix = Matrix.Identity;
}
protected override void OnContentChanged(object oldContent, object newContent)
{
base.OnContentChanged(oldContent, newContent);
if (Content is FrameworkElement element)
{
ChangeContent(element);
}
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
if (Content is FrameworkElement element)
{
ChangeContent(element);
}
SizeChanged += OnSizeChanged;
Loaded -= OnLoaded;
}
private void OnMouseLeave(object sender, MouseEventArgs e)
{
if (_capture)
{
Released();
}
}
private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (IsEnabled && !_capture)
{
Pressed(e.GetPosition(this));
}
}
private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (IsEnabled && _capture)
{
Released();
}
}
private void OnMouseMove(object sender, MouseEventArgs e)
{
if (IsEnabled && _capture)
{
var position = e.GetPosition(this);
var point = new Point
{
X = position.X - _origin.X,
Y = position.Y - _origin.Y
};
var delta = point;
_origin = position;
_matrix.Translate(delta.X, delta.Y);
Invalidate();
}
}
private void OnMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
if (IsEnabled)
{
Reset();
}
}
private void OnMouseWheel(object sender, MouseWheelEventArgs e)
{
if (IsEnabled)
{
var scale = e.Delta > 0 ? ZoomSpeed : 1 / ZoomSpeed;
var position = e.GetPosition(_content);
var x = Constrain(scale, MinZoom / _matrix.M11, MaxZoom / _matrix.M11);
var y = Constrain(scale, MinZoom / _matrix.M22, MaxZoom / _matrix.M22);
_matrix.ScaleAtPrepend(x, y, position.X, position.Y);
ZoomX = _matrix.M11;
ZoomY = _matrix.M22;
Invalidate();
}
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
if (_content?.IsMeasureValid ?? false)
{
Arrange(_content.DesiredSize, _content.RenderSize);
Invalidate();
}
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
Detatch();
SizeChanged -= OnSizeChanged;
Unloaded -= OnUnloaded;
}
private void Pressed(Point position)
{
if (IsEnabled)
{
_content.Cursor = Cursors.Hand;
_origin = position;
_capture = true;
}
}
private void Released()
{
if (IsEnabled)
{
_content.Cursor = null;
_capture = false;
}
}
private void Reset()
{
_matrix = Matrix.Identity;
if (_content != null)
{
Arrange(_content.DesiredSize, _content.RenderSize);
}
Invalidate();
}
}
./Themes/Viewport.xaml:
<ResourceDictionary ... >
<Style TargetType="{x:Type controls:Viewport}"
BasedOn="{StaticResource {x:Type ContentControl}}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:Viewport}">
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}">
<Grid ClipToBounds="True"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}">
<Grid x:Name="PART_Container">
<ContentPresenter x:Name="PART_Presenter" />
</Grid>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
./App.xaml
<Application ... >
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="./Themes/Viewport.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
Usage:
<viewers:Viewport>
<Image Source="{Binding}"/>
</viewers:Viewport>
Any issues, give me a shout.
Happy coding :)
One addition to the superb solution provided by #Wiesław Šoltés answer above
The existing code resets the image position using right click, but I am more accustomed to doing that with a double click. Just replace the existing child_MouseLeftButtonDown handler:
private void child_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (child != null)
{
var tt = GetTranslateTransform(child);
start = e.GetPosition(this);
origin = new Point(tt.X, tt.Y);
this.Cursor = Cursors.Hand;
child.CaptureMouse();
}
}
With this:
private void child_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if ((e.ChangedButton == MouseButton.Left && e.ClickCount == 1))
{
if (child != null)
{
var tt = GetTranslateTransform(child);
start = e.GetPosition(this);
origin = new Point(tt.X, tt.Y);
this.Cursor = Cursors.Hand;
child.CaptureMouse();
}
}
if ((e.ChangedButton == MouseButton.Left && e.ClickCount == 2))
{
this.Reset();
}
}

Categories