How do I add a color effect to a UI element?
For example, it should look more yellow, so pixels have a more yellow color.
All I need is to make my black form a bit white, while it is inactive.
I recently needed a gradient effect that would go from a specified color to a lighter version of that color. I came across this post which works very nicely.
Here is the code as an extension method
public static Color Interpolate(this Color color1, Color color2, float percentage)
{
double a1 = color1.A / 255.0, r1 = color1.R / 255.0, g1 = color1.G / 255.0, b1 = color1.B / 255.0;
double a2 = color2.A / 255.0, r2 = color2.R / 255.0, g2 = color2.G / 255.0, b2 = color2.B / 255.0;
byte a3 = Convert.ToByte((a1 + (a2 - a1) * percentage) * 255);
byte r3 = Convert.ToByte((r1 + (r2 - r1) * percentage) * 255);
byte g3 = Convert.ToByte((g1 + (g2 - g1) * percentage) * 255);
byte b3 = Convert.ToByte((b1 + (b2 - b1) * percentage) * 255);
return Color.FromArgb(a3, r3, g3, b3);
}
In my case I mix in 50 % white
BackgroundColor.Interpolate(Colors.White, .5f);
Based upon your clarifications, the effect you want to achieve is to place a translucent veneer over the client area, and adjust its appearance programmatically. The technique for this is to use the WPF Grid. This control allows for layering. Here's a Xaml fragment that sets up two layers...
<Window.Resources>
<SolidColorBrush Color="Yellow" x:Key="MyVeneerBrush"/>
</Window.Resources>
<Grid>
<Grid Background="{StaticResource MyVeneerBrush}" Opacity="{Binding VeneerOpacity}"/>
<Grid>
<DockPanel>
<!--Layout goes here-->
<TextBlock Text="Hello" FontSize="52"/>
</DockPanel>
</Grid>
</Grid>
The first layer contains the veneer and the second layer contains the content. Opacity on the first layer can be set from 0 (totally transparent) to 1 (totally visible), and in between values will give a translucent quality. You would need to write your ViewModel along these lines...
public class ViewModel :INotifyPropertyChanged
{
public ViewModel()
{
TurnVeneerOn();
}
private void TurnVeneerOff()
{
VeneerOpacity = 0;
}
private void TurnVeneerOn()
{
VeneerOpacity = 0.4;
}
private double _veneerOpacity;
public double VeneerOpacity
{
[DebuggerStepThrough]
get { return _veneerOpacity; }
[DebuggerStepThrough]
set
{
if (value != _veneerOpacity)
{
_veneerOpacity = value;
OnPropertyChanged("VeneerOpacity");
}
}
}
#region INotifyPropertyChanged Implementation
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string name)
{
var handler = System.Threading.Interlocked.CompareExchange(ref PropertyChanged, null, null);
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
#endregion
}
This VM exposes a property that binds to the View and controls the opacity of they first layer in the Xaml. There's two indicative methods thrown in to help get you started.
You will need to experiment with the window background and the brush and various levels of opacity to get the exact effect you are after, but there's enough here to get how it works.
The key is to use the Grid's layering capability.
For enhancing your yellow pixels, you could blend in a colored layer Photoshop style. For Photoshop style blend modes, you could use Cory Plotts' library and experiment with different blend modes. However, this may be a bit overkill for what you want to do; in which case you should try the following:
If you want your element to have a simple faded look, simply add a semi-transparent layer on top of your element as keftmedei suggested. Here's an example:
<yourElement Width="100" Height="100" Canvas.Top="50" />
This would change to:
<Grid Width="100" Height="100" Canvas.Top="50">
<yourElement />
<Rectangle Fill="#60FFFFFF" />
</Grid>
Change the opacity level by changing the 60 in Fill="#60FFFFFF"
Related
I have a problem with WPF grid. Need some help.
I have a grid splitted in two columns (e.g. "col1" and "col2" from left to right). This grid is bound to the window edges with no padding:
|--- col1 ---|--- col2 ---| ← right window border
How can I make these columns resize together with the app window in certain order?
I mean this:
when I shrink the window from the right border I need col2 to resize in first place. When col2 reaches its MinWidth — then col1 begins to shrink (I continue to move the right border of the window).
Is it possible to define the order in which columns change their size?
Or may be I need something else but the grid?
Thanks.
Understanding how to handle events and fire off your own is a cornerstone of understanding C# so I highly recommend you read up on it. That being said, this will get you started, although I'm not sure this is exactly what you want... See example code below. I leave writing code for when the window width is increased as an exercise to you.. Good luck =).
On my MainWindow I set Height="250" Width="600" and the Grid inside it gets two columns...
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="_col1" MinWidth="200" />
<ColumnDefinition x:Name="_col2" MinWidth="150"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="_txtCol1Width"
Grid.Column="0"
Background="Crimson" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
<TextBlock x:Name="_txtCol2Width"
Grid.Column="1"
Background="Turquoise" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
</Grid>
x
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.Loaded += MainWindow_Loaded;
this.SizeChanged += MainWindow_SizeChanged;
}
void MainWindow_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (this.IsLoaded && e.WidthChanged)
{
double changeInWindowWidth = e.NewSize.Width - e.PreviousSize.Width;
if (changeInWindowWidth < 0)
{
if (_col2.ActualWidth + changeInWindowWidth >= _col2.MinWidth)
{
// col 2 has not yet reached its minimum (decrease col2, no change for col1)
_col2.Width = new GridLength(_col2.ActualWidth + changeInWindowWidth, GridUnitType.Pixel);
_col1.Width = new GridLength(_col1.ActualWidth + 0, GridUnitType.Pixel);
}
else if (_col1.ActualWidth + changeInWindowWidth >= _col1.MinWidth)
{
// col 2 has reached its minimum, but col1 has not (decrease col1, no change for col2)
_col1.Width = new GridLength(_col1.ActualWidth + changeInWindowWidth, GridUnitType.Pixel);
_col2.Width = new GridLength(_col2.ActualWidth + 0, GridUnitType.Pixel);
}
else
{
// both columns have reached their minimum, so decrease width of both equally
_col1.MinWidth = _col1.ActualWidth + 0.5 * changeInWindowWidth;
_col2.MinWidth = _col2.ActualWidth + 0.5 * changeInWindowWidth;
_col1.Width = new GridLength(_col1.MinWidth, GridUnitType.Pixel);
_col2.Width = new GridLength(_col2.MinWidth, GridUnitType.Pixel);
}
}
else
{
// todo: handle window width increased ...
}
UpdateTexts();
}
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
UpdateTexts();
}
private void UpdateTexts()
{
_txtCol1Width.Text = String.Format("column {0}\nActualWidth: {1}\n(MinWidth: {2})", 1, _col1.ActualWidth, _col1.MinWidth);
_txtCol2Width.Text = String.Format("column {0}\nActualWidth: {1}\n(MinWidth: {2})", 2, _col2.ActualWidth, _col2.MinWidth);
}
}
This is sample code to draw ellipse, with shadow enabled. I set both Fill and shadow color as same. But in view shadow color is different. This may be WPF feature but in my scenario i want to set desired shadow color for the object.
<Window x:Class="Test.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
<Canvas>
<Ellipse Width="200" Height="300" Fill="#7D00FE">
<Ellipse.Effect>
<DropShadowEffect
ShadowDepth="5"
Color="#7D00FE"/>
</Ellipse.Effect>
</Ellipse>
</Canvas>
</Grid>
</Window>
It seems like that DropShadowEffect somehow affects the Color when it renders itself. This problem seems to be non-existing for primary colors (so named Colors, like Red, Blue, Aqua, etc. - but you don't have to use the name, you can specify them through #AARRGGBB format as well.)
I could not figure out the exact modification it does, nor can I offer a workaround (except to use named colors...), but I thought maybe it's worth noting it in an answer.
See this other questions, which probably point to the same "bug" or undocumented feature of DropShadowEffect:
DropShadowEffect with DynamicResource as color has weak
visibility
WPF DropShadowEffect - Unexpected Color Difference
Update:
So, this is cheating, but for your specific question, it might solve the issue:
<Grid>
<Canvas>
<Ellipse Width="200" Height="300" Fill="#7D00FE">
<Ellipse.Effect>
<DropShadowEffect
ShadowDepth="5"
Color="#BA00FE"/>
</Ellipse.Effect>
</Ellipse>
</Canvas>
</Grid>
With a little invested work, one might be able to come up with a converter, that can convert a Color to an other Color, which will be the desired DropShadowEffect Color for the given Color. If I will have a little time I will come back to this.
My intuition suggests that the problem might be in the shader code for that particular effect, and that the output might differ on different hardware (and/or driver version), but currently I can not prove this.
Update:
I was wrong about named colors, it does not work for all of those, e.g.: Green is flawed, but the problem is not - solely - dependent on the green component of the Color. Intriguing.
Update 2:
So here is the converter I talked about earlier:
using System;
using System.Windows.Data;
using System.Windows.Media;
namespace MyCustomConverters
{
public class ColorToShadowColorConverter: IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
// Only touch the shadow color if it's a solid color, do not mess up other fancy effects
if (value is SolidColorBrush)
{
Color color = ((SolidColorBrush)value).Color;
var r = Transform(color.R);
var g = Transform(color.G);
var b = Transform(color.B);
// return with Color and not SolidColorBrush, otherwise it will not work
// This means that most likely the Color -> SolidBrushColor conversion does the RBG -> sRBG conversion somewhere...
return Color.FromArgb(color.A, r, g, b);
}
return value;
}
private byte Transform(byte source)
{
// see http://en.wikipedia.org/wiki/SRGB
return (byte)(Math.Pow(source / 255d, 1 / 2.2d) * 255);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException("ColorToShadowColorConverter is a OneWay converter.");
}
#endregion
}
}
And here is how it should be used:
Resources part:
<namespaceDefinedByXmlnsProperty:ColorToShadowColorConverter x:Key="ColorConverter" />
Real usage:
<Ellipse Width="50" Height="100" Fill="#7D00FE">
<Ellipse.Effect>
<DropShadowEffect ShadowDepth="50"
Color="{Binding Fill, RelativeSource={RelativeSource
Mode=FindAncestor, AncestorType={x:Type Ellipse}},
Converter={StaticResource ColorConverter}}"/>
</Ellipse.Effect>
</Ellipse>
Thanks for Michal Ciechan for his answer, as it guided me in the right direction.
Somewhere it is converting the DropShadowEffect into a specific Sc value.
The closer to 1 you are, the less the difference (hence FF/255/1 works absolutely fine) because nth root of 1 is 1
From looking into this and researching about on ScRGB, the gamma value of ScRGB is around 2.2. Therefore when converting from RGB to ScRGB, you may need to divide by 255, then nth(2.2) root of the value to come up with the final value.
E.g.
value 5E is 94
94 / 255 = 0.36862745098039215686274509803922
2.2root of 94/255 = 0.635322735100355
0.635322735100355 * 255 = ~162 = A2
Therefore when you set the Green of the foreground to 5E, you need to set the DropShadowEffect to A2.
This is just my observation and what i came up with from my research.
Why did MS implement it like this? I HAVE NO IDEA
Sources:
RGB/XYZ Matrices
Wikipedia sRGB
Therefore in your example to have the same colour you need to use #B800FE
As explained in Ciechan's answer(thanks to Mr Ciechan), Microsoft converts the DropShadowEffect into a specific Sc value.
So how to solve it?
Just let Microsoft do the calculation back by entering the RGB value into sRGB.
//Where the variable color is the expected color.
Color newColor = new Color();
newColor.ScR = color.R;
newColor.ScG = color.G;
newColor.ScB = color.B;
//the converted color value is in newColor.R, newColor.G, newColor.B
Refer to Update 2 in #qqbenq's answer, for technical details for the binding converter(thanks to #qqbenq).
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
// Only touch the shadow color if it's a solid color, do not mess up other fancy effects
if (value is SolidColorBrush)
{
Color color = ((SolidColorBrush)value).Color;
//Where the variable color is the expected color.
Color newColor = new Color();
newColor.ScR = (float)color.R / 255;
newColor.ScG = (float)color.G / 255;
newColor.ScB = (float)color.B / 255;
return newColor;
}
return value;
}
Here is a formula improved for #qqbenq 's answer.
The changes are in the Transform function. It is much more accurate and the difference is around 1 value.
Therefore in questioner example to have the same colour you need to use #BA00FF and you will get #7D00FF (questioner requested for #7D00FE).
Source of reference for the formula found in https://www.nayuki.io/page/srgb-transform-library
private byte Transform(byte source)
{
// see http://en.wikipedia.org/wiki/SRGB
return (byte)(Math.Pow(source / 255d, 1 / 2.2d) * 255);
double x = (double)source / 255;
if (x <= 0)
return 0;
else if (x >= 1)
return 1;
else if (x < 0.0031308f)
return (byte)(x * 12.92f * 255);
else
return (byte)((Math.Pow(x, 1 / 2.4) * 1.055 - 0.055) * 255);
}
So one of my latest side projects is developing a application detection and populating assistant. Programmatically I am absolutely fine populating the backend code for what I want accomplished. But I've run into a road block on the GUI. I need a GUI that is a Quarter circle that extends from the task bar to the bottom right of a standard windows operating system. When the user doubleclicks on the application, the circle rotates into view. I can do this with a typical windows form that has a transparent background and a fancy background image. But the square properties of the form will still apply when the user has the application open. And I do not want to block the user from higher priority apps when the circle is open.
I'm not really stuck on any one specific programming language. Although, I would prefer that it not contain much 3d rendering as it is supposed to be a computing assistant and should not maintain heavy RAM/CPU consumption whilst the user is browsing around.
Secondarily, I would like the notches of the outer rings to be mobile and extend beyond the gui a mere centimeter or so.
I would not be here if I hadn't had scoured the internet for direction on this capability. But what I've found is application GUI's of this nature tend to be most used in mobile environments.
So my questions are: How can I accomplish this? What programming language can I write this in? Is this a capability currently available? Will I have to sacrifice user control for design?
I wrote out some code doing something close to what you described.
I’m not sure to understand how you do want the circle to appear, so I just let a part of it always visible.
And I didn’t get the part about the mobile outer ring.
Creating and placing the window
The XAML is very simple, it just needs a grid to host the circle’s pieces, and some attributes to remove window decorations and taskbar icon:
<Window x:Class="circle.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Circle"
Width="250"
Height="250"
AllowsTransparency="True"
Background="Transparent"
MouseDown="WindowClicked"
ShowInTaskbar="False"
WindowStyle="None">
<Grid Name="Container"/>
</Window>
To place the window in the bottom right corner, you can use SystemParameters.WorkArea in the constructor:
public MainWindow()
{
InitializeComponent();
var desktopDim = SystemParameters.WorkArea;
Left = desktopDim.Right - Width;
Top = desktopDim.Bottom - Height;
}
Creating the shape
I build the circle as a bunch of circle pieces that I generate from code behind:
private Path CreateCirclePart()
{
var circle = new CombinedGeometry
{
GeometryCombineMode = GeometryCombineMode.Exclude,
Geometry1 = new EllipseGeometry { Center = _center, RadiusX = _r2, RadiusY = _r2 },
Geometry2 = new EllipseGeometry { Center = _center, RadiusX = _r1, RadiusY = _r1 }
};
var sideLength = _r2 / Math.Cos((Math.PI/180) * (ItemAngle / 2.0));
var x = _center.X - Math.Abs(sideLength * Math.Cos(ItemAngle * Math.PI / 180));
var y = _center.Y - Math.Abs(sideLength * Math.Sin(ItemAngle * Math.PI / 180));
var triangle = new PathGeometry(
new PathFigureCollection(new List<PathFigure>{
new PathFigure(
_center,
new List<PathSegment>
{
new LineSegment(new Point(_center.X - Math.Abs(sideLength),_center.Y), true),
new LineSegment(new Point(x,y), true)
},
true)
}));
var path = new Path
{
Fill = new SolidColorBrush(Colors.Cyan),
Stroke = new SolidColorBrush(Colors.Black),
StrokeThickness = 1,
RenderTransformOrigin = new Point(1, 1),
RenderTransform = new RotateTransform(0),
Data = new CombinedGeometry
{
GeometryCombineMode = GeometryCombineMode.Intersect,
Geometry1 = circle,
Geometry2 = triangle
}
};
return path;
}
First step is to build two concentric circles and to combine them in a CombinedGeometry with CombineMode set to exclude. Then I create a triangle just tall enough to contain the section of the ring that I want, and I keep the intersection of these shapes.
Seeing it with the second CombineMode set to xor may clarify:
Building the circle
The code above uses some instance fields that make it generic: you can change the number of pieces in the circle or their radius; it will always fill the corner.
I then populate a list with the required number of shape, and add them to the grid:
private const double MenuWidth = 80;
private const int ItemCount = 6;
private const double AnimationDelayInSeconds = 0.3;
private readonly Point _center;
private readonly double _r1, _r2;
private const double ItemSpacingAngle = 2;
private const double ItemAngle = (90.0 - (ItemCount - 1) * ItemSpacingAngle) / ItemCount;
private readonly List<Path> _parts = new List<Path>();
private bool _isOpen;
public MainWindow()
{
InitializeComponent();
// window in the lower right desktop corner
var desktopDim = SystemParameters.WorkArea;
Left = desktopDim.Right - Width;
Top = desktopDim.Bottom - Height;
_center = new Point(Width, Height);
_r2 = Width;
_r1 = _r2 - MenuWidth;
Loaded += (s, e) => CreateMenu();
}
private void CreateMenu()
{
for (var i = 0; i < ItemCount; ++i)
{
var part = CreateCirclePart();
_parts.Add(part);
Container.Children.Add(part);
}
}
ItemSpacingAngle define the blank between two consecutive pieces.
Animating the circle
The final step is to unfold the circle. Using a rotateAnimation over the path rendertransform make it easy.
Remember this part of the CreateCirclePart function:
RenderTransformOrigin = new Point(1, 1),
RenderTransform = new RotateTransform(0),
The RenderTransform tells that the animation we want to perform is a rotation, and RenderTransformOrigin set the rotation origin to the lower right corner of the shape (unit is percent).
We can now animate it on click event:
private void WindowClicked(object sender, MouseButtonEventArgs e)
{
for (var i = 0; i < ItemCount; ++i)
{
if (!_isOpen)
UnfoldPart(_parts[i], i);
else
FoldPart(_parts[i], i);
}
_isOpen = !_isOpen;
}
private void UnfoldPart(Path part, int pos)
{
var newAngle = pos * (ItemAngle + ItemSpacingAngle);
var rotateAnimation = new DoubleAnimation(newAngle, TimeSpan.FromSeconds(AnimationDelayInSeconds));
var tranform = (RotateTransform)part.RenderTransform;
tranform.BeginAnimation(RotateTransform.AngleProperty, rotateAnimation);
}
private void FoldPart(Path part, int pos)
{
var rotateAnimation = new DoubleAnimation(0, TimeSpan.FromSeconds(AnimationDelayInSeconds));
var tranform = (RotateTransform)part.RenderTransform;
tranform.BeginAnimation(RotateTransform.AngleProperty, rotateAnimation);
}
Not actually answering this, but I liked your question enough that I wanted to get a minimal proof of concept together for fun and I really enjoyed doing it so i thought I'd share my xaml with you:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ed="http://schemas.microsoft.com/expression/2010/drawing" x:Class="WpfApplication1.Window2"
Title="Window2" Height="150" Width="150" Topmost="True" MouseLeftButtonDown="Window2_OnMouseLeftButtonDown"
AllowsTransparency="True"
OpacityMask="White"
WindowStyle="None"
Background="Transparent" >
<Grid>
<ed:Arc ArcThickness="40"
ArcThicknessUnit="Pixel" EndAngle="0" Fill="Blue" HorizontalAlignment="Left"
Height="232" Margin="33,34,-115,-116" Stretch="None"
StartAngle="270" VerticalAlignment="Top" Width="232" RenderTransformOrigin="0.421,0.471"/>
<Button HorizontalAlignment="Left" VerticalAlignment="Top" Width="41" Margin="51.515,71.385,0,0" Click="Button_Click" RenderTransformOrigin="0.5,0.5">
<Button.Template>
<ControlTemplate>
<Path Data="M50.466307,88.795148 L61.75233,73.463763 89.647286,102.42368 81.981422,113.07109 z"
Fill="DarkBlue" HorizontalAlignment="Left" Height="39.606"
Stretch="Fill" VerticalAlignment="Top" Width="39.181"/>
</ControlTemplate>
</Button.Template>
</Button>
</Grid>
</Window>
And it looks like this:
Im working on flowchart kind of application in asp.net using silverlight.. Im a beginner in Silvelight, Creating the elements (Rectangle,Ellipse,Line.. ) dynamically using SHAPE and LINE Objects in codebehind (c#)
These shapes will be generated dynamically, meaning I'll be calling a Web service on the backend to determine how many objects/shapes need to be created. Once this is determined, I'll need to have the objects/shapes connected together.
how to connect dynamically created shapes with a line in Silverlight like a flowchart.
I read the below article, but its not working for me, actualHeight & actualWidth of shapes values are 0.
Connecting two shapes together, Silverlight 2
here is my MainPage.xaml
<UserControl x:Class="LightTest1.MainPage">
<Canvas x:Name="LayoutRoot" Background="White">
<Canvas x:Name="MyCanvas" Background="Red"></Canvas>
<Button x:Name="btnPush" Content="AddRectangle" Height="20" Width="80" Margin="12,268,348,12" Click="btnPush_Click"></Button>
</Canvas>
code behind MainPage.xaml.cs
StackPanel sp1 = new StackPanel();
public MainPage()
{
InitializeComponent();
sp1.Orientation = Orientation.Vertical;
MyCanvas.Children.Add(sp1);
}
Rectangle rect1;
Rectangle rect2;
Line line1;
private void btnPush_Click(object sender, RoutedEventArgs e)
{
rect1 = new Rectangle()
{
Height = 30,
Width = 30,
StrokeThickness = 3,
Stroke = new SolidColorBrush(Colors.Red),
};
sp1.Children.Add(rect1);
rect2 = new Rectangle()
{
Height = 30,
Width = 30,
StrokeThickness = 3,
Stroke = new SolidColorBrush(Colors.Red),
};
sp1.Children.Add(rect2);
connectShapes(rect1, rect2);
}
private void connectShapes(Shape s1, Shape s2)
{
var transform1 = s1.TransformToVisual(s1.Parent as UIElement);
var transform2 = s2.TransformToVisual(s2.Parent as UIElement);
var lineGeometry = new LineGeometry()
{
StartPoint = transform1.Transform(new Point(1, s1.ActualHeight / 2.0)),
EndPoint = transform2.Transform(new Point(s2.ActualWidth, s2.ActualHeight / 2.0))
};
var path = new Path()
{
Data = lineGeometry,
Stroke = new SolidColorBrush(Colors.Green),
};
sp1.Children.Add(path);
}
what I am doing in button click event is just adding two rectangle shapes and tring to connect them with a line (like flowchart).
Please suggest what is wrong in my code..
Try replacing the line
connectShapes(rect1, rect2);
with
Dispatcher.BeginInvoke(() => connectShapes(rect1, rect2));
I'm not sure of the exact reason why this works, but I believe the shapes are only rendered once control passes out of your code, and only once they are rendered do the ActualWidth and ActualHeight properties have a useful value. Calling Dispatcher.BeginInvoke calls your code a short time later; in fact, you may notice the lines being drawn slightly after the rectangles.
The TransformToVisual method behaves in much the same way as the ActualWidth and ActualHeight properties. It will return an identity transformation if the shape hasn't been rendered. Even if your lines were being drawn with a definite width and height, they would end up being drawn all on top of one another at the top-left.
I also found that I needed to add the lines to the Canvas, not the StackPanel, in order for them to be drawn over the rectangles. Otherwise the StackPanel quickly filled up with lines with a lot of space above them.
I have a Canvas inside a ScrollViewer. I want to have the user to be able to grab the canvas and move it around, with the thumbs on the scrollbars updating appropriately.
My initial implementation calculates the offset on each mouse move, and updates the scrollbars:
// Calculate the new drag distance
Point newOffsetPos = e.GetPosition(MapCanvas);
System.Diagnostics.Debug.WriteLine(" newOffsetPos : " + newOffsetPos.X + " " + newOffsetPos.Y);
double deltaX = newOffsetPos.X - _offsetPosition.X ;
double deltaY = newOffsetPos.Y - _offsetPosition.Y ;
System.Diagnostics.Debug.WriteLine(" delta X / Y : " + deltaX + " " + deltaY);
System.Diagnostics.Debug.WriteLine(" sv offsets X / Y : " + _scrollViewer.HorizontalOffset + " " + _scrollViewer.VerticalOffset);
_scrollViewer.ScrollToHorizontalOffset(_scrollViewer.HorizontalOffset - deltaX);
_scrollViewer.ScrollToVerticalOffset(_scrollViewer.VerticalOffset - deltaY);
_offsetPosition = newOffsetPos;
While this works, it is not very smooth.
Is there a better way to do this? If Transforms are used, will the scrollbars update automagically when the Canvas is moved?
Thanks for any tips...
Actually this sort of problem is really a matter of using the right pattern to track the mouse. I've seen this issue in variety of cases not just in Silverlight.
The best pattern is to trap the original locations of both mouse and subject, then recalculate the new offset from the fixed original values. That way the mouse stays planted solid at a single point on the image being panned. Here is my simple Repro:-
Start with a fresh Silverlight Application in visual studio. Modify MainPage.Xaml thus:-
<UserControl x:Class="SilverlightApplication1.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid x:Name="LayoutRoot" Background="White">
<ScrollViewer x:Name="Scroller" HorizontalScrollBarVisibility="Auto">
<Image x:Name="Map" Source="test.jpg" Width="1600" Height="1200" />
</ScrollViewer>
</Grid>
</UserControl>
Add the following code to the MainPage.xaml.cs file:-
public MainPage()
{
InitializeComponent();
Map.MouseLeftButtonDown += new MouseButtonEventHandler(Map_MouseLeftButtonDown);
}
void Map_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Point mapOrigin = new Point(Scroller.HorizontalOffset, Scroller.VerticalOffset);
Point mouseOrigin = e.GetPosition(Application.Current.RootVisual);
MouseEventHandler moveHandler = null;
MouseButtonEventHandler upHandler = null;
moveHandler = (s, args) =>
{
Point mouseNew = args.GetPosition(Application.Current.RootVisual);
Scroller.ScrollToHorizontalOffset(mapOrigin.X - (mouseNew.X - mouseOrigin.X));
Scroller.ScrollToVerticalOffset(mapOrigin.Y - (mouseNew.Y - mouseOrigin.Y));
};
upHandler = (s, args) =>
{
Scroller.MouseMove -= moveHandler;
Scroller.MouseLeftButtonUp -= upHandler;
};
Scroller.MouseMove += moveHandler;
Scroller.MouseLeftButtonUp += upHandler;
}
}
Give it a reasonably large test.jpg (doesn't need to be 1600x1200 Image will scale it).
You'll note that when dragging the mouse remains exactly over a fixed point in the image until you hit a boundary. Move the mouse as fast as you like it always tracks, this is because it doesn't depend on deltas being accurate and up-to-date. The only variable is the current mouse position, the other values remain fixed as they were at mouse down.
You could try this (or at least take a peek at how it's implemented).