I'm quite new to WPF so I'm learning most of this the hard (but fun) way. I'm constructing a HSV-like colorpicker usercontrol, and would like to obtain the behaviour were the thumb I use as the "selector" is limited to an elliptic area (circle actually). When moved outside, the selector should stick to the side and not move at all. I believe this is the most common GUI-behaviour, so that is how it should behave. Feel free to suggest a better behaviour!
Is there a common, known and recommended solution to this or is it up to everyone to re-invent the wheel each time?
Any good ideas of how to solve this?
Code-behind:
public partial class HSVColorPicker : UserControl
{
public HSVColorPicker()
{
InitializeComponent();
}
void onDragDelta(object sender, DragDeltaEventArgs e)
{
Canvas.SetLeft(thumb, Canvas.GetLeft(thumb) + e.HorizontalChange);
Canvas.SetTop(thumb, Canvas.GetTop(thumb) + e.VerticalChange);
}
}
XAML:
<Grid>
<Canvas x:Name="canvas">
<Image x:Name="wheel" Source="colorwheel.png" Width="300" Margin="5,5,0,0"/>
<Thumb Name="thumb" DragDelta="onDragDelta" Canvas.Left="104" Canvas.Top="68" Template="{StaticResource thumbTemplate}" />
</Canvas>
</Grid>
While I'm in here, the thumb is always dragging behind the cursor, is there another way to create this? As I said, I'm new to WPF and GUI-making altogether so maybe there are obvious solutions that haven't occurred to me ;)
I did some rethinking and dropped the Thumb completely, using a dummy circle (named thumb) instead. Now im listening to, mousedown, mouseup and mousemove on the canvas and determine what should be possible and not. This has the nice feature that the thumb sticks to the colorwheel edge when the mouse goes outside the area, but the area is a bit larger than the colorwheel, to make it easy to get a point on the border. Not complete but it solved my question so I post it as it is at this moment.
private bool mousePressed { get; set; }
private bool mouseWithinArea { get; set; }
private Point circleMiddlePoint { get; set; }
private int margin;
private double mPX;
private double mPY;
private double localXpos;
private double globalXpos
{
get
{
return localXpos + mPX;
}
set
{
localXpos = value - mPX;
Canvas.SetLeft(thumb, value);
}
}
private double localYpos;
private double globalYpos
{
get
{
return mPY - localYpos;
}
set
{
localYpos = mPY - value;
Canvas.SetTop(thumb, value);
}
}
public HSVColorPicker()
{
InitializeComponent();
wheel.Width = 300;
margin = 15;
mPX = 150+margin;
mPY = 150+margin;
circleMiddlePoint = new Point(mPX, mPY);
}
private void CalcPosition(double X, double Y)
{
double radius = wheel.Width / 2.0;
double vectorX = X - mPX;
double vectorY = Y - mPY;
double distance = Math.Sqrt(vectorX * vectorX + vectorY * vectorY);
if (distance > radius)
{
double factor = radius / distance;
vectorX *= factor;
vectorY *= factor;
}
globalXpos = vectorX + mPX;
globalYpos = vectorY + mPY;
}
private void wheel_MouseDown(object sender, MouseButtonEventArgs e)
{
if (mouseWithinArea)
{
mousePressed = true;
Point mousePoint = e.GetPosition(this);
CalcPosition(mousePoint.X, mousePoint.Y);
}
}
private void wheel_MouseMove(object sender, MouseEventArgs e)
{
Point mousePoint = e.GetPosition(this);
double relX = mousePoint.X - mPX;
double relY = mPY - mousePoint.Y;
if (mouseWithinArea)
{
if (Math.Sqrt(relX * relX + relY * relY) > 150+margin)
{
mouseWithinArea = false;
}
else
{
if (mousePressed)
{
CalcPosition(mousePoint.X, mousePoint.Y);
}
}
}
else
{
if (Math.Sqrt(relX * relX + relY * relY) < 150+margin)
{
mouseWithinArea = true;
if (mousePressed)
{
CalcPosition(mousePoint.X, mousePoint.Y);
}
}
}
}
private void wheel_MouseUp(object sender, MouseButtonEventArgs e)
{
mousePressed = false;
}
}
<Canvas x:Name="canvas" Background="Transparent" MouseDown="wheel_MouseDown" MouseMove="wheel_MouseMove" MouseUp="wheel_MouseUp" Width="330" Height="330">
<Image x:Name="wheel" Source="colorwheel.png" Width="300" Margin="15,15,0,0" />
<Ellipse Margin="0,0,0,0"
x:Name="outerEll"
Stroke="Silver"
StrokeThickness="15"
Width="330"
Height="330"/>
<Ellipse Name="thumb" Stroke="Black" Fill="Silver" Canvas.Left="150" Canvas.Top="150" Width="15" Height="15" Margin="-12" />
</Canvas>
You want the center of your thumb to lay within your color wheel.
So, the distance between the center of your thumb and the center of your color wheel (that is, the center of your canvas) must be less than
or equal to the radius of your color wheel (that is, half the side of your canvas).
Untested c# code:
void onDragDelta(object sender, DragDeltaEventArgs e)
{
double radius = canvas.RenderSize.Width / 2.0;
double thumbCenterX = Canvas.GetLeft(thumb) - thumb.RenderSize.Width + e.HorizontalChange;
double thumbCenterY = Canvas.GetTop(thumb) - thumb.RenderSize.Height + e.VerticalChange;
double colorWheelCenterX = canvas.RenderSize.Width / 2.0;
double colorWheelCenterY = canvas.RenderSize.Height / 2.0;
double vectorX = thumbCenterX - colorWheelCenterX;
double vectorY = thumbCenterY - colorWheelCenterY;
double distance = Math.Sqrt(vectorX * vectorX + vectorY * vectorY);
if(distance > radius) {
double factor = radius / distance;
vectorX *= factor;
vectorY *= factor;
}
Canvas.SetLeft(thumb, colorWheelCenterX + vectorX - thumb.RenderSize.Width / 2.0);
Canvas.SetTop(thumb, colorWheelCenterY + vectorY - thumb.RenderSize.Height / 2.0);
}
Related
I have a canvas (OuterCanvas) within a canvas (InnerCanvas) and am handling zooming in an out as follows:
private ScaleTransform ScaleTransform = new ScaleTransform();
private void OuterCanvas_MouseWheel(Object sender, MouseWheelEventArgs e)
{
if (e.Delta >= 1)
{
ScaleTransform.ScaleX += 0.03;
ScaleTransform.ScaleY += 0.03;
}
else
{
ScaleTransform.ScaleX -= 0.03;
ScaleTransform.ScaleY -= 0.03;
}
ScaleTransform.CenterX = ActualWidth / 2;
ScaleTransform.CenterY = ActualHeight / 2;
InnerCanvas.RenderTransform = TransformGroup;
}
This zooms in and out quite okay at the centre of the canvas. I'd like to zoom in and out around the mouse pointer, so change the CenterX and CenterY lines as follows:
ScaleTransform.CenterX = e.GetPosition(this).X;
ScaleTransform.CenterY = e.GetPosition(this).Y;
Again this works satisfactory, however if I attempt to move the mouse during two zoom events (mouse wheel events), the whole canvas jumps rather drastically as the scale factor increases or decreases.
Demonstration of problem
This is the same issue as when the CenterX and CenterY properties are set to the ActualWidth/2 and ActualHeight/2 and the window size is changed.
What should the correct handling be of the CenterX and CenterY properties be to avoid this issue?
Here is a more or less complete solution, including the possibility to pan the inner Canvas:
<Canvas x:Name="outerCanvas"
MouseWheel="OnMouseWheel"
MouseMove="OnMouseMove"
MouseLeftButtonDown="OnMouseLeftButtonDown"
MouseLeftButtonUp="OnMouseLeftButtonUp">
<Canvas x:Name="innerCanvas"
Width="400" Height="400" Background="AliceBlue">
<Canvas.RenderTransform>
<MatrixTransform />
</Canvas.RenderTransform>
</Canvas>
</Canvas>
with these event handlers:
private double zoomScaleFactor = 1.1;
private Point? mousePos;
private void OnMouseWheel(object sender, MouseWheelEventArgs e)
{
var pos = e.GetPosition(outerCanvas);
var scale = e.Delta > 0 ? zoomScaleFactor : 1 / zoomScaleFactor;
var transform = (MatrixTransform)innerCanvas.RenderTransform;
var matrix = transform.Matrix;
matrix.ScaleAt(scale, scale, pos.X, pos.Y);
transform.Matrix = matrix;
}
private void OnMouseMove(object sender, MouseEventArgs e)
{
if (mousePos.HasValue)
{
var pos = e.GetPosition(outerCanvas);
var delta = pos - mousePos.Value;
var transform = (MatrixTransform)innerCanvas.RenderTransform;
var matrix = transform.Matrix;
matrix.Translate(delta.X, delta.Y);
transform.Matrix = matrix;
mousePos = pos;
}
}
private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (outerCanvas.CaptureMouse())
{
mousePos = e.GetPosition(outerCanvas);
}
}
private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
outerCanvas.ReleaseMouseCapture();
mousePos = null;
}
I need to create a control like this.
Click on this link to see the image
How to do that ?
I got some help to start with. But, still not sure how to do it.
In xaml I have something like this:
<Grid Background="White"
MouseUp="ParentOnMouseUp">
<Canvas x:Name="canvas"
Background="Green"
Width="200"
Height="200"
HorizontalAlignment="Center"
VerticalAlignment="Center"
MouseMove="CanvasOnMouseMove">
<Ellipse x:Name="dot"
Width="20"
Height="20"
Fill="Blue"
Loaded="DotOnLoaded"
MouseDown="DotOnMouseDown"/>
</Canvas>
In the code behind file I have the following code
private void DotOnMouseDown(object sender, MouseButtonEventArgs e)
{
_isDraggingDot = true;
}
private void CanvasOnMouseMove(object sender, MouseEventArgs e)
{
if (_isDraggingDot)
{
var mousePos = e.GetPosition(canvas);
var x = mousePos.X;
if (x < 0)
{
x = 0;
}
if (x > canvas.Width)
{
x = canvas.Width;
}
var y = mousePos.Y;
if (y < 0)
{
y = 0;
}
if (y > canvas.Height)
{
y = canvas.Height;
}
dot.SetValue(Canvas.LeftProperty, x - (dot.Width / 2.0)); // offset ensures dot is centred on mouse pointer
dot.SetValue(Canvas.TopProperty, y - (dot.Height / 2.0));
}
}
private void ParentOnMouseUp(object sender, MouseButtonEventArgs e)
{
_isDraggingDot = false;
//CentreDot();
}/
private void CentreDot()
{
dot.SetValue(Canvas.LeftProperty, (canvas.Width / 2.0) - (dot.Width / 2.0));
dot.SetValue(Canvas.TopProperty, (canvas.Height / 2.0) - (dot.Height / 2.0));
}
private void DotOnLoaded(object sender, RoutedEventArgs e)
{
CentreDot();
}
I am not able to draw the horizontal scale of -180 to +180 and the vertical scale of -30 to +30. Also, once the dot is moved to a specific point. I need the co-ordinates of that point.
I would like to rotate the border with inertia. Where do I leave something out?
MainPage.xaml:
<Grid>
<Border x:Name="ManipulationBorder" Width="200" Height="200" HorizontalAlignment="Center" VerticalAlignment="Center" Background="Red"/>
</Grid>
MainPage.xaml.cs:
private TransformGroup transforms;
private MatrixTransform previousTransform;
private CompositeTransform deltaTransform;
public MainPage()
{
this.InitializeComponent();
InitManipulationTransforms();
ManipulationBorder.ManipulationDelta += new ManipulationDeltaEventHandler(ManipulateMe_ManipulationDelta);
ManipulationBorder.ManipulationMode =
ManipulationModes.TranslateX |
ManipulationModes.TranslateY |
ManipulationModes.Rotate |
ManipulationModes.TranslateInertia |
ManipulationModes.RotateInertia;
}
private void InitManipulationTransforms()
{
transforms = new TransformGroup();
previousTransform = new MatrixTransform() { Matrix = Matrix.Identity };
deltaTransform = new CompositeTransform();
transforms.Children.Add(previousTransform);
transforms.Children.Add(deltaTransform);
ManipulationBorder.RenderTransform = transforms;
}
private void ManipulateMe_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
{
previousTransform.Matrix = transforms.Value;
// Center point for rotation
Point center = previousTransform.TransformPoint(new Point(e.Position.X, e.Position.Y));
deltaTransform.CenterX = center.X;
deltaTransform.CenterY = center.Y;
// Rotation
deltaTransform.Rotation = e.Delta.Rotation;
}
When I actually go to apply delta rotation in ManipulationDeltaRoutedEventArgs it does not work. Where am I wrong?
Thanks in advance.
When I actually go to apply delta rotation in ManipulationDeltaRoutedEventArgs it does not work. Where am I wrong?
The standard touch-based rotation requires two touch points or more:
For single touch rotation you need fix the center of deltaTransform firstly.
You need recalculate the Angle has been changed with single touch.
you will know the value of Angle = a1 - a2.
private void Right_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
{
var x = this.RightRotateTransform.CenterX - e.Position.X;
var y = this.RightRotateTransform.CenterY - e.Position.Y;
double a1 = Math.Atan(y / x);
double a2 = Math.Atan((e.Delta.Translation.Y - y) / (x - e.Delta.Translation.X));
this.RightRotateTransform.Angle += a1 - a2;
}
My program can draw lines using canvas.Drawline(). How to click line and change this color (select line)?
private List<Point> coordFirst = new List<Point>();
private List<Point> coordLast = new List<Point>();
public Graphics canvas;
private void Form1_Load(object sender, EventArgs e)
{
canvas=panel1.CreateGraphics();
}
Coordinate line stored in coordFirs & coodLast.
Here is a suitable Line class:
class Line
{
public Color LineColor { get; set; }
public float Linewidth { get; set; }
public bool Selected { get; set; }
public Point Start { get; set; }
public Point End { get; set; }
public Line(Color c, float w, Point s, Point e)
{ LineColor = c; Linewidth = w; Start = s; End = e; }
public void Draw(Graphics G)
{ using (Pen pen = new Pen(LineColor, Linewidth)) G.DrawLine(pen, Start, End); }
public bool HitTest(Point Pt)
{
// test if we fall outside of the bounding box:
if ((Pt.X < Start.X && Pt.X < End.X) || (Pt.X > Start.X && Pt.X > End.X) ||
(Pt.Y < Start.Y && Pt.Y < End.Y) || (Pt.Y > Start.Y && Pt.Y > End.Y))
return false;
// now we calculate the distance:
float dy = End.Y - Start.Y;
float dx = End.X - Start.X;
float Z = dy * Pt.X - dx * Pt.Y + Start.Y * End.X - Start.X * End.Y;
float N = dy * dy + dx * dx;
float dist = (float)( Math.Abs(Z) / Math.Sqrt(N));
// done:
return dist < Linewidth / 2f;
}
}
Define a List for the lines, probably at class level:
List<Line> lines = new List<Line>();
Here is how you can initialize it with a few lines:
for (int i = 0; i < 20; i++) lines.Add(new Line(Color.Black, 4f,
new Point(R.Next(panel1.Width), R.Next(panel1.Height)),
new Point(R.Next(panel1.Width), R.Next(panel1.Height))));
Here is the result of clicking on a crossing:
Whenever you add, change or remove a line you need to make the Panel reflect the news by triggering the Paint event:
panel1.Invalidate();
Here is the Paint event of the Panel:
private void panel1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
foreach (Line L in lines) L.Draw(e.Graphics);
}
In the MouseClick event you do the test:
private void panel1_MouseClick(object sender, MouseEventArgs e)
{
foreach(Line L in lines)
L.LineColor = L.HitTest(e.Location) ? Color.Red : Color.Black;
panel1.Invalidate();
}
To avoid flicker don't use the basic Panel class as it isn't doublebuffered. Instead use either a PictureBox or a Label (with AutoSize=false) or a doublebuffered Panel subclass:
class DrawPanel : Panel
{ public DrawPanel () { DoubleBuffered = true; } }
Notes:
There is no such thing as a 'Line' in WinForms, only pixels of various colors. So to select a line you need to store it's two endpoints' coordinates and then find out if you have hit it when clicking.
The above example shows how to do it in math.
Instead one could test each line by drawing it onto a bitmap and test the pixel the mouse has clicked. But drawing those bitmaps would have to do math behind the scenes as well and also allocate space for the bitmaps, so the math will be more efficient..
Yes the Line class looks a little long for such a simple thing a s a line but look how short all the event codes now are! That's because the responsiblities are where they belong!
Also note the the first rule of doing any drawing in WinForms is: Never cache or store a Grahics object. In fact you shouldn't ever use CreateGraphics in the first place, as the Graphics object will never stay in scope and the graphics it produces will not persist (i.e. survive a Minimize-maximize sequence)..
Also note how I pass out the e.Graphics object of the Paint event's parameters to the Line instances so they can draw themselves with a current Graphics object!
To select thinner lines it may help to modify the distance check a little..
The Math was taken directly form Wikipedia.
You can change the color of everything on click. By using click event of particular object.
I give you an example for button. If you click on button then panal’s color will be change. You can modify the code as per your requirement.
private List<Point> coordFirst = new List<Point>();
private List<Point> coordLast = new List<Point>();
public Graphics canvas;
private void Form1_Load(object sender, EventArgs e)
{
canvas = panel1.CreateGraphics();
}
private void panel1_Click(object sender, EventArgs e)
{
panel1.BackColor = Color.Blue;
}
private void nonSelectableButton3_Click(object sender, EventArgs e)
{
panel1.BackColor = Color.BurlyWood;
}
I created a draggaable user control in silverlight, but having issues in moving the control inside page. Actually the issue is, dragged control moves out of page when dragging which i don't want. I want the control should drag inside the parent control only.
Any help/suggestion would be appreciated.
Below is the code used to perform operation:
XAML:
<Grid x:Name="LayoutRoot" >
<Grid.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform x:Name="LocalTranslateTransform"/>
</TransformGroup>
</Grid.RenderTransform>
C#:
private void OnToolbarClicked(object sender, MouseButtonEventArgs e)
{
mouseDownInToolbar = true;
DragOffset = e.GetPosition(LayoutRoot);
toolbarBorder.CaptureMouse();
}
private void OnToolbarMoving(object sender, MouseEventArgs e)
{
if (mouseDownInToolbar)
{
// we want to move it based on the position of the mouse
moveUserControl(e);
}
}
private void moveUserControl (MouseEventArgs e)
{
Point mousePos = e.GetPosition(LayoutRoot);
Double newX = LocalTranslateTransform.X + (mousePos.X - DragOffset.X);
Double newY = LocalTranslateTransform.Y + (mousePos.Y - DragOffset.Y);
LocalTranslateTransform.X = newX;
LocalTranslateTransform.Y = newY;
}
private void OnToolbarReleased(object sender, MouseButtonEventArgs e)
{
mouseDownInToolbar = false;
toolbarBorder.ReleaseMouseCapture();
}
When you use TranslateTransform, you're sending instructions to the compositor thread to display the element at X/Y offsets. It does not obey the rules of your panel and any visual tree member. So, you have to tell the compositor thread about the boundaries. Use Clip like this:
<Grid Width="500" Height="500">
<Grid.Clip>
<RectangleGeometry Rect="0,0,500,500" />
</Grid.Clip>
<Grid x:Name="LayoutRoot">
<!-- This is the element being dragged by the mouse -->
</Grid>
</Grid>
The RectangleGeometry will create a boundary where child elements are allowed to appear. Anything outside, will be clipped.
After doing the desired changes this is how it looked.
You need to create a constraint.
Calculate if you're transforming the actual control outside the bounds of the parent.
private void moveUserControl (MouseEventArgs e)
{
Point mousePos = e.GetPosition(LayoutRoot);
Double newX = LocalTranslateTransform.X + (mousePos.X - DragOffset.X);
Double newY = LocalTranslateTransform.Y + (mousePos.Y - DragOffset.Y);
var minX = 0;
var maxX = 500 - ActualWidth; // 500 is parent width
var minY = 0;
var maxY = 500 - ActualHeight; // 500 is parent height
if (newX < minX)
{
newX = minX;
}
else if (newX > maxX)
{
newX = maxX;
}
if (newY < minY)
{
newY = minY;
}
else if (newY > maxY)
{
newY = maxY;
}
LocalTranslateTransform.X = newX;
LocalTranslateTransform.Y = newY;
}