I am experiencing a weird problem with a render transform in WPF. The project I'm working on needs to display a clicked user point over an image. When the user clicks a point, a custom control is placed at the location of their click. The image should then be able to be scaled around any point using the mouse wheel, and the custom control should be translated (not scaled) to the correct location.
To do this, I follow the MouseWheel event as follows:
private void MapPositioner_MouseWheel(object sender, MouseWheelEventArgs e)
{
Point location = Mouse.GetPosition(MainWindow.Instance.imageMap);
MainWindow.Instance.imageMap.RenderTransform = null;
ScaleTransform st = new ScaleTransform(scale + (e.Delta < 0 ? -0.2 : 0.2), scale += (e.Delta < 0 ? -0.2 : 0.2));
st.CenterX = location.X;
st.CenterY = location.Y;
TransformGroup tg = new TransformGroup();
tg.Children.Add(st);
//tg.Children.Add(tt);
MainWindow.Instance.imageMap.RenderTransform = tg;
if (scale <= 1)
{
MainWindow.Instance.imageMap.RenderTransform = null;
}
if (TransformationChanged != null)
TransformationChanged();
}
Then, I implemented an event handler in the custom control for the TransformationChanged event seen at the end of the above code block as follows:
private void Instance_TransformationChanged()
{
//check image coords
//
if (MainWindow.Instance.imageMap.RenderTransform != null)
{
if (MainWindow.Instance.imageMap.RenderTransform != Transform.Identity)
{
Transform st = MainWindow.Instance.imageMap.RenderTransform;
Point image = MainWindow.VideoOverlayCanvas.TransformToVisual(MainWindow.Instance.MapImage).Transform(loc2);
Point trans = st.Transform(image);
Point final = MainWindow.Instance.MapImage.TransformToVisual(MainWindow.VideoOverlayCanvas).Transform(trans);
// selected = anchor2;
// final = ClipToOverlay(final);
// selected = null;
connector.X2 = final.X;
connector.Y2 = final.Y;
Canvas.SetLeft(anchor2, final.X);
Canvas.SetTop(anchor2, final.Y);
}
}
else
{
connector.X2 = loc2.X;
connector.Y2 = loc2.Y;
Canvas.SetLeft(anchor2, loc2.X);
Canvas.SetTop(anchor2, loc2.Y);
}
}
This way, I can ensure that the custom control's position is updated only after the new transform is set. Note that since I am applying the transform to the point, there is no scaling done to the control, the effect is that it is translated to the point it should. This works fine as long as the user is only scaling around one point. If they change that point, it doesnt work.
Here are some images that show the problem:
User clicks a point
user zooms out, what happened here?
after zooming out (all the way out in this case) it looks ok
I've been messing with this for about two days now, so I apologize if my code looks messy. I know this is a pretty obscure question so any help would be appreciated.
Thanks,
Max
If anyone is looking for an answer to this, because of deadlines, I had to write a workaround by having the user pan with the right mouse button and zoom with the mouse wheel. This way zooming always happens around the center of the image, so the controls are always lined up. I'm still looking for answers to the original question though if anyone can figure it out
Thanks,
Max
I'm not sure what's wrong with your transform, but have you considered an alternate approach? For example, you might want to add a transparent canvas set to stay at the same size as the image, z-order above the image (explicitly set or just put the Canvas element just after the image element). Then you can just use Canvas.SetLeft and Canvas.SetTop to place the user control where the user clicked, and to move it around. A lot easier than using a transform.
Related
I'm writing a plugin for a trading software (C#, winforms, .NET 3.5) and I'd like to draw a crosshair cursor over a panel (let's say ChartPanel) which contains data that might be expensive to paint. What I've done so far is:
I added a CursorControl to the panel
this CursorControl is positioned over the main drawing panel so that it covers it's entire area
it's Enabled = false so that all input events are passed to the parent
ChartPanel
it's Paint method is implemented so that it draws lines from top to bottom and from left to right at current mouse position
When MouseMove event is fired, I have two possibilities:
A) Call ChartPanel.Invalidate(), but as I said, the underlying data may be expensive to paint and this would cause everything to redraw everytime I move a mouse, which is wrong (but it is the only way I can make this work now)
B) Call CursorControl.Invalidate() and before the cursor is drawn I would take a snapshot of currently drawn data and keep it as a background for the cursor that would be just restored everytime the cursor needs to be repainted ... the problem with this is ... I don't know how to do that.
2.B. Would mean to:
Turn existing Graphics object into Bitmap (it (the Graphics) is given to me through Paint method and I have to paint at it, so I just can't create a new Graphics object ... maybe I get it wrong, but that's the way I understand it)
before the crosshair is painted, restore the Graphics contents from the Bitmap and repaint the crosshair
I can't control the process of painting the expensive data. I can just access my CursorControl and it's methods that are called through the API.
So is there any way to store existing Graphics contents into Bitmap and restore it later? Or is there any better way to solve this problem?
RESOLVED: So after many hours of trial and error I came up with a working solution. There are many issues with the software I use that can't be discussed generally, but the main principles are clear:
existing Graphics with already painted stuff can't be converted to Bitmap directly, instead I had to use panel.DrawToBitmap method first mentioned in #Gusman's answer. I knew about it, I wanted to avoid it, but in the end I had to accept, because it seems to be the only way
also I wanted to avoid double drawing of every frame, so the first crosshair paint is always drawn directly to the ChartPanel. After the mouse moves without changing the chart image I take a snapshow through DrawToBitmap and proceed as described in chosen answer.
The control has to be Opaque (not enabled Transparent background) so that refreshing it doesn't call Paint on it's parent controls (which would cause the whole chart to repaint)
I still experience occasional flicker every few seconds or so, but I guess I can figure that out somehow. Although I picked Gusman's answer, I would like to thank everyone involved, as I used many other tricks mentioned in other answers, like the Panel.BackgroundImage, use of Plot() method instead of Paint() to lock the image, etc.
This can be done in several ways, always storing the graphics as a Bitmap. The most direct and efficient way is to let the Panel do all the work for you.
Here is the idea: Most winforms Controls have a two-layered display.
In the case of a Panel the two layers are its BackgroundImage and its Control surface.
The same is true for many other controls, like Label, CheckBox, RadioButton or Button.
(One interesting exception is PictureBox, which in addition has an (Foreground) Image. )
So we can move the expensive stuff into the BackgroundImage and draw the crosshair on the surcafe.
In our case, the Panel, all nice extras are in place and you could pick all values for the BackgroundImageLayout property, including Tile, Stretch, Center or Zoom. We choose None.
Now we add one flag to your project:
bool panelLocked = false;
and a function to set it as needed:
void lockPanel( bool lockIt)
{
if (lockIt)
{
Bitmap bmp = new Bitmap(panel1.ClientSize.Width, panel1.ClientSize.Width);
panel1.DrawToBitmap(bmp, panel1.ClientRectangle);
panel1.BackgroundImage = bmp;
}
else
{
if (panel1.BackgroundImage != null)
panel1.BackgroundImage.Dispose();
panel1.BackgroundImage = null;
}
panelLocked = lockIt;
}
Here you can see the magic at work: Before we actually lock the Panel from doing the expensive stuff, we tell it to create a snapshot of its graphics and put it into the BackgroundImage..
Now we need to use the flag to control the Paint event:
private void panel1_Paint(object sender, PaintEventArgs e)
{
Size size = panel1.ClientSize;
if (panelLocked)
{
// draw a full size cross-hair cursor over the whole Panel
// change this to suit your own needs!
e.Graphics.DrawLine(Pens.Red, 0, mouseCursor.Y, size.Width - 1, mouseCursor.Y);
e.Graphics.DrawLine(Pens.Red, mouseCursor.X, 0, mouseCursor.X, size.Height);
}
// expensive drawing, you insert your own stuff here..
else
{
List<Pen> pens = new List<Pen>();
for (int i = 0; i < 111; i++)
pens.Add(new Pen(Color.FromArgb(R.Next(111),
R.Next(111), R.Next(111), R.Next(111)), R.Next(5) / 2f));
for (int i = 0; i < 11111; i++)
e.Graphics.DrawEllipse(pens[R.Next(pens.Count)], R.Next(211),
R.Next(211), 1 + R.Next(11), 1 + R.Next(11));
}
}
Finally we script the MouseMove of the Panel:
private void panel1_MouseMove(object sender, MouseEventArgs e)
{
mouseCursor = e.Location;
if (panelLocked) panel1.Invalidate();
}
using a second class level variable:
Point mouseCursor = Point.Empty;
You call lockPanel(true) or lockPanel(false) as needed..
If you implement this directly you will notice some flicker. This goes away if you use a double-buffered Panel:
class DrawPanel : Panel
{
public DrawPanel() { this.DoubleBuffered = true; }
}
This moves the crosshair over the Panels in a perfectly smooth way. You may want to turn on & off the Mouse cursor upon MouseLeave and MouseEnter..
Why don't you clone all the graphics in the ChartPanel over your CursorControl?
All the code here must be placed inside your CursorControl.
First, create a property which will hold a reference to the chart and hook to it's paint event, something like this:
ChartPanel panel;
public ChartPanel Panel
{
get{ return panel; }
set{
if(panel != null)
panel.Paint -= CloneAspect;
panel = value;
panel.Paint += CloneAspect;
}
}
Now define the CloneAspect function which will render the control's appearance to a bitmap whenever a Paint opperation has been done in the Chart panel:
Bitmap aspect;
void CloneAspect(object sender, PaintEventArgs e)
{
if(aspect == null || aspect.Width != panel.Width || aspect.Height != panel.Height)
{
if(aspect != null)
aspect.Dispose();
aspect = new Bitmap(panel.Width, panel.Height, System.Drawing.Imaging.PixelFormat.Format32bppPArgb);
}
panel.DrawToBitmap(aspect, new Rectangle(0,0, panel.Width, panel.Height);
}
Then in the OnPaint overriden method do this:
public override void OnPaint(PaintEventArgs e)
{
e.Graphics.DrawImage(aspect);
//Now draw the cursor
(...)
}
And finally wherever you create the chart and the customcursor you do:
CursorControl.Panel = ChartPanel;
And voila, you can redraw as many times you need without recalculating the chart's content.
Cheers.
I'm currently trying to create a little plot interactive editor, using WPF.
On maximized window the plot dragging with mouse is not responsive enough because of the plot grid.
I got a path for my plot grid lying inside a Canvas control (render transform just shifts it to the bottom of the canvas)
<Path Name="VisualGrid" RenderTransform="{StaticResource PlotTechnicalAdjust}" Style="{DynamicResource ResourceKey=GridStyle}" Panel.ZIndex="1"/>
Here is how grid is created; _curState has actual camera "viewport" metadata
if (_curState.Changes.ScaleStepXChanged)
{
foreach (TextBlock item in _xLabels)
{
DeleteLabel(item);
}
_xLabels.Clear();
double i = _curState.LeftEdgeLine;
_gridGeom.Children[(int)GridGeomIndexes.VerticalLines] = new GeometryGroup { Transform = _verticalLinesShift};
var verticalLines =(GeometryGroup)_gridGeom.Children[(int)GridGeomIndexes.VerticalLines];
while (i <= _curState.RightEdgeLine * (1.001))
{
verticalLines.Children.Add(new LineGeometry(new Point(i * _plotParameters.PixelsPerOneX, 0),
new Point(i * _plotParameters.PixelsPerOneX,
-_wnd.ContainerGeneral.Height)));
_xLabels.Add(CreateLabel(i, Axis.X));
i += _curState.CurrentScaleStepX;
}
_curState.Changes.ScaleStepXChanged = false;
}
if (_curState.Changes.ScaleStepYChanged)
{
foreach (TextBlock item in _yLabels)
{
DeleteLabel(item);
}
_yLabels.Clear();
double i = _curState.BottomEdgeLine;
_gridGeom.Children[(int)GridGeomIndexes.HorizontalLines] = new GeometryGroup { Transform = _horizontalLinesShift};
var horizontalLines = (GeometryGroup)_gridGeom.Children[(int)GridGeomIndexes.HorizontalLines];
while (i <= _curState.TopEdgeLine * (1.001))
{
horizontalLines.Children.Add(new LineGeometry(new Point(0, -i * _plotParameters.PixelsPerOneY),
new Point(_wnd.ContainerGeneral.Width,
-i * _plotParameters.PixelsPerOneY)));
_yLabels.Add(CreateLabel(i, Axis.Y));
i += _curState.CurrentScaleStepY;
}
_curState.Changes.ScaleStepYChanged = false;
}
Where Transforms are composition of TranslateTransform and ScaleTransform (for vertical lines I only use X components and only Y for horizontal lines).
After beeing created those GeometryGroups are only edited if a new line apears into camera or an existing line exits viewable space. Grid is only recreated when axis graduations have to be changed after zooming.
I have a dragging option implemented like this:
private Point _cursorOldPos = new Point();
private void OnDragPlotMouseMove(object sender, MouseEventArgs e)
{
if (e.Handled)
return;
Point cursorNewPos = e.GetPosition(ContainerGeneral);
_plotView.TranslateShiftX.X += cursorNewPos.X - _cursorOldPos.X;
_plotView.TranslateShiftY.Y += cursorNewPos.Y - _cursorOldPos.Y;
_cursorOldPos = cursorNewPos;
e.Handled = true;
}
This works perfectly smooth with a small window (1200x400 units) for a large amount of points (like 100+).
But for a large window (fullscreen 1920x1080) it happens pretty jittery even without any data-point controls on canvas.
The strange moment is that lags don't appear when I order my GridGenerator to keep around 100+ lines for small window and drag performance suffers when I got less than 50 lines on maximezed. It makes me think that it might somehow depend not on a number of elements inside a geometry, but on their linear size.
I suppose I should mention that OnSizeChanged I adjust the ContainerGeneral canvas' height and width and simply re-create the grid.
Checked the number of lines stored in runtime to make sure I don't have any extras. Tried using Image with DrawingVisual instead of Path. Nothing helped.
Appearances for clearer understanding
It was all about stroke dashes and WPF's unhealthy desire to count them all while getting hit test bounds for DrawingContext.
The related topic is Why does use of pens with dash patterns cause huge (!) performance degredation in WPF custom 2D drawing?
I'm using the ViewportControl to scroll around and zoom in and out of my Map. In this map I've got a green ellipse which I wish to move around. Previously I used a ScrollViewer where I set the manipulationMode of the ScrollViewer to control, and thus making it capable of moving my ellipse around. However I can't find a similar way for the ViewportControl. So is there a way to move my ellipse around?
The code I've got so far is:
The xaml part where I have my ViewportControl around my map
<ViewportControl x:Name="ViewPortTestTest" Bounds="0,0,1271,1381.5" Height="480" Width="800" Canvas.ZIndex="1" Grid.Row="1">
<ViewportControl.RenderTransform>
<CompositeTransform x:Name="myTransformTest"/>
</ViewportControl.RenderTransform>
<View:Map x:Name="ZoomableContent" >
<View:Map.RenderTransform>
<CompositeTransform x:Name="myTransform" />
<!-- ScaleX="{Binding Map.imageScale}" ScaleY="{Binding Map.imageScale}"/>-->
</View:Map.RenderTransform>
</View:Map>
</ViewportControl>
It is in the map where I add the ellipse. The viewModel where I manipulate my ellipse
public void ManStart(ManipulationStartedEventArgs e)
{
e.Handled = true;
ViewportControl VP = FindParentOfType<ViewportControl>(ChampViewModelSel);
}
}
public void ManDelta(ManipulationDeltaEventArgs e)
{
e.Handled = true;
Point fingerPosition = e.DeltaManipulation.Translation;
Temp.x = fingerPosition.X;
Temp.y = fingerPosition.Y;
}
}
Where Temp.x and Temp.y is the new position of the ellipse.
I think you could try to use TouchPanel from XNA and Touch.FrameReported event for this purpose. Probably Map and VieportControl handle the manipulation event so it won't fire with your code.
In my proposal Touch_FrameReported will be fired every time you touch the screen, so we have to check only for situations we need - here comes IsGestureAvailable and TouchAction. Simple code can look like this:
public MainPage()
{
InitializeComponent();
TouchPanel.EnabledGestures = GestureType.FreeDrag;
Touch.FrameReported += Touch_FrameReported;
}
private void Touch_FrameReported(object sender, TouchFrameEventArgs e)
{
if (TouchPanel.IsGestureAvailable) // check only dragging
{
// get point relative to Viewport
TouchPoint mainTouch = e.GetPrimaryTouchPoint(ViewPortTestTest);
// check if drag has completed (key up)
if (mainTouch.Action == TouchAction.Up)
{
Temp.x = mainTouch.Position.X;
Temp.y = mainTouch.Position.Y;
}
}
}
You can read more about Gestures here at MSDN and on this blog.
You can also check other GestureTypes and for example check if TouchAction.Down and user clicked on your Ellipse.
These methods give you many possibilities to read TouchPoints and their Actions so maybe it will help.
It took me a while to find a way how to disable Vieport. I've found a little 'hack' for that, it will be easier if your VieportControl was only horizontal or vertical (there is a lock property, but doesn't work for both). Here is this little 'hack' which should disable both horizontal and vertical scrolling:
Rect originalBounds = new Rect();
private void Touch_FrameReported(object sender, TouchFrameEventArgs e)
{
TouchPoint myTouchPoint = e.GetPrimaryTouchPoint(ViewPortTestTest);
// disable scrolling
if (myTouchPoint.Action == TouchAction.Down) // here probably some other statement like if user hit ellipse
{
originalBounds = ViewPortTestTest.Bounds;
ViewPortTestTest.Bounds = ViewPortTestTest.Viewport; // set current view as Bounds
}
// more logic
// enable once again
if (myTouchPoint.Action == TouchAction.Up)
ViewPortTestTest.Bounds = originalBounds;
}
When I want to disable scrolling - I set bounds of the VieportControl to the current view (and I remember original ones). When I want to enable scrolling - I bring back original bounds.
As I've tested it - it is working - of course it needs a little more logic in locking if statement, so that it won't lock every time you touch the screen.
I'm using C# chart control to draw a nyquist plot. Now i want data points appear on the curve each time the user moves the mouse on it. So i used hit test method in GetToolTipText event.
private void BodePlot_GetToolTipText(object sender, ToolTipEventArgs e)
{
HitTestResult result = BodePlot.HitTest(e.X, e.Y);
selectDataPoint = null;
if (result.ChartElementType == ChartElementType.DataPoint)
{
selectDataPoint = (DataPoint)result.Object;
e.Text = selectDataPoint.ToString();
}
{
The problem is only a part of the curve shows values, others don't. When i use e.Text = result.Object.ToString(); to get the object on which the mouse is pointing to, here what i found :
Instead of showing the data points, the text on tooltip show custom label. So i guess the reason is that the curve is covered by the labels of x and y axis.
The only solution that i found is disabling the x and y axis, with that everything works fine. But i want to keep those axes, so how can i make those labels hide under the curve.
Your analysis is likely correct. The way to go around this would be to provide HitTest() with the optional third argument which define the desired element type.
public HitTestResult HitTest (
int x,
int y,
ChartElementType requestedElement
)
This should return underlying data points even if other elements are overlapping them.
I'm busy with a small application in which I want to display information at the location of the cursor when it hoovers over a Canvas. The Canvas in question is a custom one (inherited from Canvas) which provides functionality to add DrawingVisuals (as shown in basically every tutorial on displaying large amounts of geometric shapes on a canvas).
I would like to display a vertical line and horizontal line as well as the local coordinates (p in the code below) which are directly derived from the canvas coordinates (v). At the moment I'm rendering these objects at position (0,0) and use offset during the OnMouseMove event to update their location.
The horizontal and vertical lines are rendered in the DrawingVisual _cursor and the location in local y,z-coordinates in _info.
private void oCanvas_MouseMove(object sender, MouseEventArgs e)
{
#region 1. Get location data
System.Windows.Vector v = (System.Windows.Vector)e.GetPosition(oCanvas);
// point in YZ coordinates
BSMath.DoubleXY p = new BSMath.DoubleXY();
p.X = (oCanvas.OriginY - v.Y) / oCanvas.ZoomFactor;
p.Y = (oCanvas.OriginX - v.X) / oCanvas.ZoomFactor;
#endregion
#region 2. Update cursor and info
if (oSettings.ShowInformation)
{
_info.Info = p.X.ToString("0.0") + " | " + p.Y.ToString("0.0");
_info.Render(0, 0);
_info.Visual.Offset = v;
}
// move cursor
_cursor.Visual.Offset = v;
}
Using the mousemove event seems to be creating a lot of overhead and I can see that there are issues tracking the mouse movements when I move the mouse quickly.
Can anyone recommend a better way of creating the same effect?
example http://www.iccg.be/test/images/canvas.jpg
Edit:
I investigated it a bit further and the problem seems to occur when the resolution of the canvas is bigger. If it is a 600x400 canvas then there is no delay, but when it is around 1000x800 I get the problem with delays when hoovering. The performance also improves if I use user drawn crosshairs instead of the lines that have the full width/ height of the canvas.
I recently have built something similar and haven't had any performance issues.
Did it the very simple way by adding the stuff directly on the canvas.
The drawn items are in a second canvas behind the mouse position canvas. Both reside in a Grid.
This is for sure not the most sophisticated way to solve this, but it works quite well for me.
Here's the code:
private Point _previous;
private Point _current;
private Line _xLine;
private Line _yLine;
private TextBlock _displayTextBlock;
private void Canvas_MouseMove(object sender, MouseEventArgs e)
{
_current = e.GetPosition(myCanvas);
if (_previous != _current)
{
if (_xLine == null)
{
_xLine = new Line() {X1 = 0, X2 = myCanvas.ActualWidth, Stroke = new SolidColorBrush(Colors.Black)};
_yLine = new Line() {Y1 = 0, Y2 = myCanvas.ActualHeight, Stroke = new SolidColorBrush(Colors.Black)};
_displayTextBlock = new TextBlock();
myCanvas.Children.Add(_xLine);
myCanvas.Children.Add(_yLine);
myCanvas.Children.Add(_displayTextBlock);
}
_displayTextBlock.SetValue(Canvas.TopProperty, _current.Y);
_displayTextBlock.SetValue(Canvas.LeftProperty, _current.X);
_displayTextBlock.Text = _current.X.ToString() + " | " + _current.Y.ToString();
_xLine.Y1 = _current.Y;
_xLine.Y2 = _current.Y;
_yLine.X1 = _current.X;
_yLine.X2 = _current.X;
_previous = _current;
}
}