Smooth zoom in WPF - c#

So here is my questions that I hope I will be able to ask it in a proper way.
In brief: how to zoom smoothly in WPF?
In detail: I'm writing a CAD application in WPF that I'm redrawing parts of the screen when MouseWheel event is fired.
private void ModelWindowBorder_MouseWheel(object sender, MouseWheelEventArgs e)
{
var zoom = e.Delta > 0 ? 0.2 : -0.2;
// increase or decrease the scale by *zoom*
// Redraw Screen
// Apply TransformScale and etc.
}
Well actually the above code works but I'm not content with it for two reasons:
When I have to redraw lots of visuals on the screen it kinda misses the multiple MouseWheel events that are fired. So if 8 MouseWheel events are to be fired in one big scroll only 4-5 of them are fired. And this makes the zooming a little slow.
The second reason might not be WPF related at all. Is var zoom = e.Delta > 0 ? 0.2 : -0.2; the correct way we should increase/decrease the scale? To me it seems not appropriate. Since when we are zoomed out the difference seems considerable when we zoom in by one step, but once we are close to the drawing, increasing the zoom by one step doesn't seem much.
I'd like to know your answers and comments on both of these issues.

Problem 1.
You should take in account the value of e.Delta.
Not
var zoom = e.Delta > 0 ? 0.2 : -0.2;
but
var zoom = e.Delta * k;
k is a double factor, it's device specific
The effective upper and lower ranges of this value potentially come from device implementations or other callers that raised the event, and are therefore not defined
But its value get higher for as you say one big scroll.
One possible solution to find k would be to get first MouseWheel event and use received value as a starting point. If you receive double of that value, then two or more of mousewheel scrolls were combined into single event. Or do calibration all time, remembering smallest value and changing factor for it. Up to you.
Problem 2.
Use multiplication instead of adding flat value.
And combined solution will looks like this
st.ScaleX *= e.Delta * k;

One simple thing to try and coelesce MouseWheel events is to use Rx. You can use the .FromEventPattern() method to consume Wheel events and only care about those that satisfy your delta tolerances. Rx is a little tricky but can help you have simpler code paths.
See this answer - Detect scroll is completed with PointerWheelChanged

Related

How to draw lines statically before paint event?

I'm creating a spectrum analyzer and I want to make it gridded. My spectrum will be painted over a panel in a windows forms application. When I draw lines inside panel_Paint function which is tied to panel's paint event, It redraws 2048/48000 times a second because it is a real time FFT spectrum with the FFT size of 2048 and sampling rate of 48000. Here is my way of drawing lines inside the paint function:
private void panel1_Paint(object sender, PaintEventArgs e)
{
for (int i = 0; i < panel1.Width; i+= panel1.Width / 20)
{
for (int j = 0; j < panel1.Height; j+= panel1.Height / 20)
{
g.DrawLine(Pens.Black, i, j, i, panel1.Height + j);
g.DrawLine(Pens.Black, i, j, panel1.Width + i, j);
}
}
}
Here is the spectrum after lines:
I like how spectrum looks but while computation there is no time for the spectrum while waiting for the lines to be redrawn.
Note: The above image is an instant(single fft block) screen shot of the spectrum. As I mentioned above, spectrum is not readable while performing real-time application.
Any help will be appreciated, thank you in advance.
Making a real-time application with graphics is not trivial. Several points:
Painting at 2048 Hz is not usefull for the viewer, the screen usually refreshes at 60 Hz because our eyes cannot observe faster changes.
The paint event is meant to paint everything in one go, so gridlines and the FFT graph.
You can trigger the paint event by calling Control.Invalidate() or in this case Control.Refresh() might give a more pleasant refresh. See: https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.control.invalidate
It might be necessary to move the code that collects the signal and calculates the FFT spectrum to a background thread, where you have much more control over its timing and can process the data uninterrupted.
So a possible way of organizing the application would be like this:
Have a background thread do the heavy work and collect data in a buffer data structure.
Put a timer on the UI Form that calls Control.Refresh() periodically. See: https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.timer
Have the paint event handler read all the data prepared in the buffer and clear it. And then use that data to paint a FFT graph. Combining all the data from multiple FFT blocks.
Make sure the buffer can contain multiple FFT bocks by using a suitable data structure like a ConcurrentQueue<T> where T is of a Type that contains one FFT block. See: https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentqueue-1
There is a lot of information around about how to communicate from a background thread to a foreground thread like the one that is handling the paint events. So I'm not going to duplicate that here. But done right it makes your application run smooth and responsive.
Combining multiple FFT blocks into one graph drawing is a challenge. So, at first, keep that part simple by for example discarding all but the most recent block. Then when everything else works improve on the design of this part.
Also start with a paint timer set to 20 Hz or there about and increase it from there. Because the graphics system might not like to many paint actions.
Turns out I used the wrong tools for this job, when processing real time data, you should make computations really fast before displaying it. And when It came to displaying, double buffering the parent display tool works like "magic".
As I mentioned in the question above, I was using panel over the form while drawing the spectrum, when I removed the panel and changed the whole form to a UserControl, I was able to do double buffering that I couldn't do in the panel and the whole problem was solved.
When the DoubleBuffered property is set to true, the current drawing data is written to a second buffer before being drawn on the surface on which the drawing will be made, then the data in this second buffer is quickly written to the memory where the drawing will be made. Voila! Flickering is prevented.
The code inside the UserControl's load function:
this.DoubleBuffered = true;

Horizontal Scroll Bar in Oxyplot WPF

I'm trying to implement a utility for showing throughput over time in a system, and am using Oxyplot to visualise the data.
Currently, zoom and pan are working as expected, but I would like some visual indication to the user which clearly shows whether the graph can be zoomed or panned.
After ditching the idea of using a scroll bar (being neither able to accurately get the position of the visible section of the graph, nor correctly position the thumb of the scroll bar releative to the chart), I have settled on using icons to show whether there is any data on the chart which is hidden to the left or rightmost side.
I would like these icons to work as buttons which allow the user to page left and right on the graph, however as with all things OxyPlot related, the implementation is far more complex than it first seems.
I'm using the WPF implementation, which uses a ViewModel representing the overall data set, with each series item represented by its own model.
This effectively renders almost every tutorial useless as the WPF implementation is significantly different to the basic OxyPlot package.
Currently, the code behind in the view handles the click on the page left/right buttons. I cannot put this in my ViewModel as it must interract directly with the PlotControl object.
private void btnPageRight_Click(object sender, RoutedEventArgs e) {
CategoryAxis axis = (CategoryAxis)PlotControl.ActualModel.Axes[0];
double xAxisMin = axis.ActualMinimum;
double xAxisMax = axis.ActualMaximum;
double visibleSpan = xAxisMax - xAxisMin;
double newMinOffset = xAxisMax + visibleSpan;
PlotControl.Axes[0].Minimum = newMinOffset;
PlotControl.Axes[0].Maximum = newMinOffset + visibleSpan;
PlotControl.ActualModel.InvalidatePlot(true);
}
As it stands, the above code throws no errors, but it does not work either.
If anybody can advise a possible way to make OxyPlot scroll to a given position using just code behind, I would be grateful.
As a last resort, I have pondered trying to simulate a mouse drag event to make this finicky beast behave.
I find the need to work around the problem in that way quite offensive, but desparation leads to odd solutions...
In case anybody else runs into this issue, the following snippet will scroll the graph in pages based on the number of columns visible on the graph at the time.
The snippet takes the number of visible columns as the viewport, and will move the visible area by the viewport size.
Although this applies to the WPF implementation, the only way I could find to make this work was to run this method from the code behind in the View containing the OxyPlot chart.
This should work correctly regardless of the zoom amount at the time.
The CategoryAxis reference must be obtained from the ActualModel as the WPF.Axis does not provide the ActualMinumum and ActualMaximum needed to calculate the viewable area.
The visibleSpan in this case represents the number of columns, with panStep denoting the amount to pan by in pixels.
private void ScrollInPages() {
//To zoom on the X axis.
CategoryAxis axis = (CategoryAxis)PlotControl.ActualModel.Axes[0];
double visibleSpan = axis.ActualMaximum - axis.ActualMinimum;
double panStep = 0;
//Scrolling the chart - comment out as appropriate
//Scroll right one page
panStep = axis.Transform(0 - (axis.Offset + visibleSpan));
//Scroll left one page
panStep = axis.Transform(axis.Offset + visibleSpan);
axis.Pan(panStep);
PlotControl.InvalidateFlag++;
}

What is the fastest way to draw thousands of lines in WinForms application

I have WinForms application. I made an user control, which draws a map from coordinates of ca 10k lines. Actualy, not all lines are straight ones, but when the map is zoomed out fully - Bezier curves are irrelevant and are replaced with straight lines.
When the map is zoomed, I have smaller number of lines and curves, so the drawing is fast enough (below 15ms). But when it's zoomed out fully - I need to draw all lines (because all fit into viewport). This is painfully slow. On my very fast machine it takes about 1000ms, so on slower machines it would be an overkill.
Is there a simple way to speed up the drawing?
I use Graphics object for drawing and I set Graphics.Scale property to my map fit into my control.
Does this slow things down?
I use Graphics.TranslateTransform() to ensure the whole map is visible.
Both scale and translate is set only once in OnPaint() event handler.
Then there is a loop which draws ca 10k lines. And I just see them drawing on the screen.
Maybe WPF container would help?
Well, I could probably simplify the map to merge some lines, but I wonder if it's worth the effort. It would complicate the code greatly, would introduce much more calculations, use extra memory and I don't know if at the end of the day it would be considerably faster.
BTW, I tested that processing of all lines (converting from one structure to another with some aditional calculations) takes ca 10ms on my machine. So - the drawing alone costs 100x more time.
EDIT:
Now here's the new problem. I've turned double buffering on with:
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer, true);
Here's my messy OnPaint() handler:
protected override void OnPaint(PaintEventArgs e) {
base.OnPaint(e);
if (Splines == null) return;
var pens = new[] {
new Pen(TrackColor),
new Pen(TrackColor),
new Pen(RoadColor),
new Pen(RiverColor),
new Pen(CrossColor)
};
var b = Splines.Bounds;
Graphics g = e.Graphics;
g.PageScale = _CurrentScale;
g.TranslateTransform(-b.Left, -b.Top);
int i = 0;
foreach (var s in Splines) {
g.DrawLine(pens[s.T], s.A, s.D);
if (++i > 100) break;
//if (s.L) g.DrawLine(pens[s.T], s.A, s.D);
//else g.DrawBezier(pens[s.T], s.A, s.B, s.C, s.D);
}
foreach (var p in pens) p.Dispose();
}
Take my word the code works, if I only remove OptimizedDoubleBuffer from styles. When double buffering is on the handler executes properly, each DrawLine is executed with correct params. But the graphics is not displayed. CPU usage during resizing is next to zero. Like all DrawLine calls were ignored. What's happening here?
In a related post I've seen recently but can't find, the OP claimed to have seen a large speed-up when switching his control to use double-buffering. Apparently there's a substantial hit for drawing stuff to the screen.
Another thing you could try is decimating the point lists in the lines you draw when zoomed out. Instead of doing the decimation each frame, you could do it only once each time the zoom is changed.
Try double buffering as a possible solution or try to reduce the number of lines. Only testing will give you an answer for your application.
Winforms Double Buffering
Double buffering with Panel
The feasibility of this really depends on if you're using anti-aliasing, if the thing can rotate, if the thickness has to be very accurate, etc.
However you can always draw all the lines into a bitmap, then simply redraw the bitmap unless the map data itself has actually changed. Of course then you get into having different bitmaps for different zoom levels, hiding and showing them, multiple bitmaps in a grid for the high details etc.
It's definitely not ideal, but if you really do need to draw thousands of lines on a 20ms refresh though.. it might be your only real option.
Or you could use lower level of drawing, outside GDI+. one such example is SlimDX. This wrapper allows you to create a directX device write from your windows controls and forms. Once DirectX is in action, the speed can increase up to several times.
2ndly, when drawing on win panel even with DoubleBuffered enabled, you always have to Invalidate the panel which asks the Environment to call the OnPaint event which actual draws using the system provided Graphics object. This invalidation usually requires a timer with fire rate more than 30 to five you a feeling of smooth playback. Now, when the load increases, the subsequent timer event is delayed since everything is happening under a single thread. And the timer must Yield the thread for around 25ms after every fire (windows OS limitation). Cross Thread access ia not allowed, using which a System.Threading.Timer could have prevent this jitter.
See this link for an example where I have tried to transfer my existing GDI code to DirectX. The code uses a lot of graphics attributes which i have incorporated in the wrapper which can draw on both GDI and DirectX.
https://drive.google.com/file/d/1DsoQl62x2YeZIKFxf252OTH4HCyEorsO/view?usp=drivesdk

Prevent mouse from leaving my form

I've attached some MouseMove and MouseClick events to my program and next up is one of these:
Get "global" mouse movement, so that I can read the mouse location even outside the form.
Prevent my mouse from leaving the form in the first place.
My project is a game so it'd be awesome to prevent the mouse leaving my form like most other games do (ofc. you can move it out if you switch focus with alt+tab fe.) and taking a look at answers to other questions asking for global mosue movement, they seem pretty messy for my needs.
Is there an easy way to prevent my mouse from going outside my form's borders? Or actually to prevent it from going OVER the borders in the first place, I want the mouse to stay inside the client area.
Additional info about the game:
The game is a short, 5-30 seconds long survival game (it gets too hard after 30 seconds for you to stay alive) where you have to dodge bullets with your mouse. It's really annoying when you move your mouse out of the form and then the player (System.Windows.Forms.Panel attached to mouse) stops moving and instantly gets hit by a bullet. This is why preventing mouse from leaving the area would be good.
Late answer but might come in handy. You could subscribe the form to MouseLeave and MouseMove events and handle them like this :
private int X = 0;
private int Y = 0;
private void Form1_MouseLeave(object sender, EventArgs e)
{
Cursor.Position = new Point(X, Y);
}
private void Form1_MouseMove(object sender, MouseEventArgs e)
{
if (Cursor.Position.X < this.Bounds.X + 50 )
X = Cursor.Position.X + 20;
else
X = Cursor.Position.X - 20;
if (Cursor.Position.Y < this.Bounds.Y + 50)
Y = Cursor.Position.Y + 20;
else
Y = Cursor.Position.Y - 20;
}
The above will make sure the mouse cursor never leaves the bounds of the form. Make sure you unsubscribe the events when the game is finished.
Edit :
Hans Passants's answer makes more sense than my answer. Use Cursor.Clip on MouseEnter :
private void Form1_MouseEnter(object sender, EventArgs e)
{
Cursor.Clip = this.Bounds;
}
You could free the cursor in case of any error/crash (I'm sure you could catch'em) :
Cursor.Clip = Rectangle.Empty;
You cannot trap the mouse, that would prevent the user from, say, operating the Start menu. Closest you can get is assigning the Cursor.Clip property. But it is easily defeated by the user pressing Ctrl+Esc for example, there is no notification for this.
Best thing to do is to subscribe the form's Deactivated event, it reliably tells you that the user switched to another program. The Activated event tells you when the user moved back. Of course the user will have few reasons to actually do this when the game score depends on keeping a game object moving. So don't forget to give the user an easy way to pause the game with, say, the Escape key.
I don't know a solution for your exact problem, but I have a completely different idea for you. I don't know how your game works, but based on what you told me, why not make it a step harder: Add borders to the game-area, for example 4 pixels wide rectangles, which you are not allowed to touch. If you touch them, you die and the mouse gets released.
You can use the Cursor class. For example:
int X = Cursor.Position.X;
int Y = Cursor.Position.Y;
As for preventing the user to move the mouse outside the form, the best approach would probably be if you had someway to know what is the coordinates of your form on the screen and attach a MouseMove event, and check if the mouse is inside the form rectangle.
To know the form position on the screen take a look at this question.
I wouldn't recommend the global mouse movement control for two reasons.
It's bad design, you should respect the bounds of the operating system. Make the application full screen if you want this kind of behaviour. The only applications that should perform these kind of operations are "kiosk" mode applications which lock down the entire OS (to prevent operator abuse).
Global key hooks are messy, aren't guaranteed to work and are dangerous because they affect a key part of the operating system (all controls). A bug in your code could result in requiring a reboot on the machine.
That said, last time I checked (a while ago, on Vista) SetWindowsHookEx still works (but its not officially supported IIRC), it's an unmanaged call so you'll have to pinvoke but with it you can refuse to pass on messages that would move the mouse outside of the bounds of your application. I'm not 100% sure if the OS will let you beat it to the cursor control (I've only blocked keyboards before on desktop boxes) but its probably your best shot.

Dynamic-Data-Display / Getting x-coordinate of user input

My program is basically about analyzing videos.
A major part is to plot a diagram showing (f.e.) brightness per frame on y-axis and every frame number on x-axis. Because the program is written in C# and uses WPF, D³ was the way to go for plotting.
Now the user might see a peak signal in the diagram and wants to look on that single frame to understand why it's so bright (it might be just natural, or an encoding-artifact).
There comes my question: The most intuitive way for the user to click on the diagram where the peak is, which jumps the video preview (other GUI element) right to that frame. So I need the x-coordinate (=frame number) of the user click on the diagram.
It is possible to manually analyze the mouse-input event, but that would take much work (because the x-axis is different for each video and the entire diagram can be resized, so absolute coordinates are a no go).
But maybe something similar is already implemented by D³. I searched the documentary, but didn't find anything useful. The only piece of information was using a "DraggablePoint", but that's where the trail goes cold.
Does someone of you know how to get the x-coordinate without much work?
It sure is possible! The way that I have done it in the past is to add a CursorCoordinateGraph object to my plotters children, and it automatically tracks the mouse position on the graph in relation to the data. You can turn off the visual features of the CursorCoordinateGraph and use it for tracking only. Here's what it would look like:
CursorCoordinateGraph mouseTrack;
plotter.Children.Add(mouseTrack);
mouseTrack.ShowHorizontalLine = false;
mouseTrack.ShowVerticalLine = false;
And your mouse click event would look like this:
private void plotter_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Point mousePos = mouseTrack.Position;
var transform = plotter.Viewport.Transform;
Point mousePosInData = mousePos.ScreenToData(transform);
double xValue = mousePosInData.X;
}
You can then use xValue and manipulate it however you would like to achieve your desired effect.

Categories

Resources