in my project (WinUi 3) i draw some polylines to plot some "real time" datas with Skiasharp. I saw the GPU-load is very high. So i tryed, what happens when less datas are drawn. ....the GPU load is still the same. Only if i change the size of the "SKXamlCanvas" it reduces the GPU load. (but the plot shout have a certain size...) So i tryed to render my chart in a bitmap with a lower resolution and scale it. But still it needs the same recources.
Is there any possibility to reduce the basic GPU-load? (as example an possibility to render in a lower resolution and scale it afterwords)
Example Application
(.net 6.0 Winui-3)
(Installed Modules: SkiaSharp, Skiasharp.Views.Winui)
XAML:
x:Class="skEfficientDraw.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:skEfficientDraw"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:skia="using:SkiaSharp.Views.Windows"
mc:Ignorable="d">
<Grid>
<skia:SKXamlCanvas x:Name="skCanvas" PaintSurface="skCanvas_PaintSurface" />
</Grid>
C#:
public sealed partial class MainWindow : Window
{
SKPaint linePaint = new SKPaint
{
Color = SKColor.Parse("#003366"),
StrokeWidth = 5,
IsAntialias = true,
Style = SKPaintStyle.Stroke
};
public MainWindow()
{
this.InitializeComponent();
DispatcherTimer DrawTimer;
DrawTimer = new DispatcherTimer();
DrawTimer.Tick += DrawTimer_Tick;
DrawTimer.Interval = new TimeSpan(0, 0, 0, 0, 100);
DrawTimer.Start();
}
private void DrawTimer_Tick(object sender, object e)
{
skCanvas.Invalidate();
}
private void skCanvas_PaintSurface(object sender, SkiaSharp.Views.Windows.SKPaintSurfaceEventArgs e)
{
///
/// - The GPU load is very high, also if lineCount is 0
/// - Is there a possibility to reduce the GPU-load? Maybe rendering the "skCanvas" in a lower resolution?
///
SKImageInfo info = e.Info;
SKSurface surface = e.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
int lineCount = 5;
Random rand = new Random();
for (int i = 0; i < lineCount; i++)
{
int x1 = rand.Next(info.Width);
int y1 = rand.Next(info.Height);
int x2 = rand.Next(info.Width);
int y2 = rand.Next(info.Height);
canvas.DrawLine(x1, y1, x2, y2, linePaint);
}
}
}
Related
I have to implement writing by touch on Wacom tablet ( like on paper) and I need to render up to 200 strokes per second in WPF Canvas. The problem is that after about 20s of constant writing it gets a bit laggs and the laggs grows. I store all strokes in HashSet. I thought about taking screen shoot when HashSet contains over 600 elements and set it as a background image and clear the HashSet, but after ~30 screen shoots it gets a bit blurred. Do you have any idea how to make it better?
You can use WriteableBitmap. This is very fast, but it works by writing individual bytes to bitmap, so if you need to apply effects to the drawing, then you have to write them yourself. This is an example of how you can use WriteableBitmap. It takes ~100 ms in debug mode to draw 1000 lines (each made out of 1600 points) on 1600 x 1200 px image on my computer, but I'm sure this can be optimized.
I'm just randomly drawing lines, but you can get events from Canvas and capture positions of stylus and pass them after each stroke.
public partial class MainWindow : Window
{
private WriteableBitmap _bitmap;
private readonly Random _random = new Random();
private readonly Stopwatch _stopwatch = new Stopwatch();
private const int White = 0x00000000;
private const int Red = 0x00FF0000;
private int _width;
private int _height;
public MainWindow()
{
InitializeComponent();
CanvasImage.Loaded += OnLoaded;
}
private async void OnLoaded(object sender, RoutedEventArgs e)
{
_width = (int)DrawableCanvas.ActualWidth;
_height = (int)DrawableCanvas.ActualHeight;
_bitmap = new WriteableBitmap(_width, _height, 96, 96, PixelFormats.Bgr32, null);
CanvasImage.Source = _bitmap;
while (true)
{
unsafe
{
for (var index = 0; index < _width * _height; index++)
*((int*)_bitmap.BackBuffer + index) = White;
}
_stopwatch.Start();
for (var index = 0; index < 1000; index++)
{
var start = _random.Next(0, _width);
var points = Enumerable.Range(0, _width).Select(x => new Point((x + start) % _width, x % _height));
UpdateImage(points);
}
Debug.WriteLine($"Last 1000 draws took: {_stopwatch.ElapsedMilliseconds} ms");
_stopwatch.Reset();
await Task.Delay(300);
}
}
private void UpdateImage(IEnumerable<Point> points)
{
_bitmap.Lock();
foreach (var point in points)
{
var x = (int)point.X;
var y = (int)point.Y;
var offset = _width * y + x;
unsafe
{
*((int*)_bitmap.BackBuffer + offset) = Red;
}
}
_bitmap.AddDirtyRect(new Int32Rect(0, 0, _width, _height));
_bitmap.Unlock();
}
}
And view:
<Grid>
<Border Width="1604" Height="1204" BorderThickness="2" BorderBrush="Blue">
<Canvas x:Name="DrawableCanvas">
<Image x:Name="CanvasImage"></Image>
</Canvas>
</Border>
</Grid>
I have pixel art creator program, and I have rectangles on canvas that are one field (pixel?). And this is good solution on not huge amount of it (for example 128x128). if i want to create 1024x1024 rectangles on canvas this process is very long, ram usage is about 1-2 gb and after that program runs very slowly. How to optimize this, or create better solution?
Using a Rectangle to represent each pixel is the wrong way to do this. As a FrameworkElement, every rectangle participates in layout and input hit testing. That approach is too heavy weight to be scalable. Abandon it now.
I would recommend drawing directly to a WriteableBitmap and using a custom surface to render the bitmap as the user draws.
Below is a minimum proof of concept that allows simple drawing in a single color. It requires the WriteableBitmapEx library, which is available from NuGet.
public class PixelEditor : FrameworkElement
{
private readonly Surface _surface;
private readonly Visual _gridLines;
public int PixelWidth { get; } = 128;
public int PixelHeight { get; } = 128;
public int Magnification { get; } = 10;
public PixelEditor()
{
_surface = new Surface(this);
_gridLines = CreateGridLines();
Cursor = Cursors.Pen;
AddVisualChild(_surface);
AddVisualChild(_gridLines);
}
protected override int VisualChildrenCount => 2;
protected override Visual GetVisualChild(int index)
{
return index == 0 ? _surface : _gridLines;
}
private void Draw()
{
var p = Mouse.GetPosition(_surface);
var magnification = Magnification;
var surfaceWidth = PixelWidth * magnification;
var surfaceHeight = PixelHeight * magnification;
if (p.X < 0 || p.X >= surfaceWidth || p.Y < 0 || p.Y >= surfaceHeight)
return;
_surface.SetColor(
(int)(p.X / magnification),
(int)(p.Y / magnification),
Colors.DodgerBlue);
_surface.InvalidateVisual();
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (e.LeftButton == MouseButtonState.Pressed && IsMouseCaptured)
Draw();
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
CaptureMouse();
Draw();
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
ReleaseMouseCapture();
}
protected override Size MeasureOverride(Size availableSize)
{
var magnification = Magnification;
var size = new Size(PixelWidth* magnification, PixelHeight * magnification);
_surface.Measure(size);
return size;
}
protected override Size ArrangeOverride(Size finalSize)
{
_surface.Arrange(new Rect(finalSize));
return finalSize;
}
private Visual CreateGridLines()
{
var dv = new DrawingVisual();
var dc = dv.RenderOpen();
var w = PixelWidth;
var h = PixelHeight;
var m = Magnification;
var d = -0.5d; // snap gridlines to device pixels
var pen = new Pen(new SolidColorBrush(Color.FromArgb(63, 63, 63, 63)), 1d);
pen.Freeze();
for (var x = 1; x < w; x++)
dc.DrawLine(pen, new Point(x * m + d, 0), new Point(x * m + d, h * m));
for (var y = 1; y < h; y++)
dc.DrawLine(pen, new Point(0, y * m + d), new Point(w * m, y * m + d));
dc.Close();
return dv;
}
private sealed class Surface : FrameworkElement
{
private readonly PixelEditor _owner;
private readonly WriteableBitmap _bitmap;
public Surface(PixelEditor owner)
{
_owner = owner;
_bitmap = BitmapFactory.New(owner.PixelWidth, owner.PixelHeight);
_bitmap.Clear(Colors.White);
RenderOptions.SetBitmapScalingMode(this, BitmapScalingMode.NearestNeighbor);
}
protected override void OnRender(DrawingContext dc)
{
base.OnRender(dc);
var magnification = _owner.Magnification;
var width = _bitmap.PixelWidth * magnification;
var height = _bitmap.PixelHeight * magnification;
dc.DrawImage(_bitmap, new Rect(0, 0, width, height));
}
internal void SetColor(int x, int y, Color color)
{
_bitmap.SetPixel(x, y, color);
}
}
}
Just import it into your Xaml, preferably inside a ScrollViewer:
<Window x:Class="WpfTest.PixelArtEditor"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:WpfTest"
Title="PixelArtEditor"
Width="640"
Height="480">
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<l:PixelEditor />
</ScrollViewer>
</Window>
Obviously, this is a far cry from being a fully-featured pixel art editor, but it's functional, and it's enough to get you on the right track. The difference in memory usage between editing a 128x128 image vs. 1024x1024 is about ~30mb. Fire it up and see it in action:
Hey, that was fun! Thanks for the diversion.
Just to improve Mike Strobel solution to snap gridlines to device pixels.
var d = -0.5d; // snap gridlines to device pixels
using (DrawingContext dc = _dv.RenderOpen())
{
GuidelineSet guidelineSet = new GuidelineSet();
guidelineSet.GuidelinesX.Add(0.5);
guidelineSet.GuidelinesY.Add(0.5);
dc.PushGuidelineSet(guidelineSet);
// Draw grid
}
Below follows a minimal, complete and verifiable example based on the issue I am encountering with WPF model rendering, here we are just rendering randomly distributed "particles" on an arbitrary 2D plane where each particle has a colour corresponding to its order of spawning.
MainWindow.cs
public partial class MainWindow : Window {
// prng for position generation
private static Random rng = new Random();
private readonly ComponentManager comp_manager;
private List<Color> color_list;
// counter for particle no.
private int current_particles;
public MainWindow() {
InitializeComponent();
comp_manager = new ComponentManager();
current_particles = 0;
color_list = new List<Color>();
}
// computes the colours corresponding to each particle in
// order based on a rough temperature-gradient
private void ComputeColorList(int total_particles) {
for (int i = 0; i < total_particles; ++i) {
Color color = new Color();
color.ScA = 1;
color.ScR = (float)i / total_particles;
color.ScB = 1 - (float)i / total_particles;
color.ScG = (i < total_particles / 2) ? (float)i / total_particles : (1 - (float)i / total_particles);
// populate color_list
color_list.Add(color);
}
}
// clear the simulation view and all Children of WorldModels
private void Clear() {
comp_manager.Clear();
color_list.Clear();
current_particles = 0;
// clear Model3DCollection and re-add the ambient light
// NOTE: WorldModels is a Model3DGroup declared in MainWindow.xaml
WorldModels.Children.Clear();
WorldModels.Children.Add(new AmbientLight(Colors.White));
}
private void Generate(int total) {
const int min = -75;
const int max = 75;
// generate particles
while (current_particles < total) {
int rand_x = rng.Next(min, max);
int rand_y = rng.Next(min, max);
comp_manager.AddParticleToComponent(new Point3D(rand_x, rand_y, .0), 1.0);
Dispatcher.Invoke(() => { comp_manager.Update(); });
++current_particles;
}
}
// generate_button click handler
private void OnGenerateClick(object sender, RoutedEventArgs e) {
if (current_particles > 0) Clear();
int n_particles = (int)particles_slider.Value;
// pre-compute colours of each particle
ComputeColorList(n_particles);
// add GeometryModel3D instances for each particle component to WorldModels (defined in the XAML code below)
for (int i = 0; i < n_particles; ++i) {
WorldModels.Children.Add(comp_manager.CreateComponent(color_list[i]));
}
// generate particles in separate thread purely to maintain
// similarities between this minimal example and the actual code
Task.Factory.StartNew(() => Generate(n_particles));
}
}
ComponentManager.cs
This class provides a convenience object for managing a List of Component instances such that particles can be added and updates applied to each Component in the List.
public class ComponentManager {
// also tried using an ObservableCollection<Component> but no difference
private readonly List<Component> comp_list;
private int id_counter = 0;
private int current_counter = -1;
public ComponentManager() {
comp_list = new List<Component>();
}
public Model3D CreateComponent(Color color) {
comp_list.Add(new Component(color, ++id_counter));
// get the Model3D of most-recently-added Component and return it
return comp_list[comp_list.Count - 1].ComponentModel;
}
public void AddParticleToComponent(Point3D pos, double size) {
comp_list[++current_counter].SpawnParticle(pos, size);
}
public void Update() {
// VERY SLOW, NEED WAY TO CACHE ALREADY RENDERED COMPONENTS
foreach (var p in comp_list) { p.Update(); }
}
public void Clear() {
id_counter = 0;
current_counter = -1;
foreach(var p in comp_list) { p.Clear(); }
comp_list.Clear();
}
}
Component.cs
This class represents the GUI model of a single particle instance with an associated GeometryModel3D giving the rendering properties of the particle (i.e. the material and thus colour as well as render target/visual).
// single particle of systems
public class Particle {
public Point3D position;
public double size;
}
public class Component {
private GeometryModel3D component_model;
private Point3DCollection positions; // model Positions collection
private Int32Collection triangles; // model TriangleIndices collection
private PointCollection textures; // model TextureCoordinates collection
private Particle p;
private int id;
// flag determining if this component has been rendered
private bool is_done = false;
public Component(Color _color, int _id) {
p = null;
id = _id;
component_model = new GeometryModel3D { Geometry = new MeshGeometry3D() };
Ellipse e = new Ellipse {
Width = 32.0,
Height = 32.0
};
RadialGradientBrush rb = new RadialGradientBrush();
// set colours of the brush such that each particle has own colour
rb.GradientStops.Add(new GradientStop(_color, 0.0));
// fade boundary of particle
rb.GradientStops.Add(new GradientStop(Colors.Black, 1.0));
rb.Freeze();
e.Fill = rb;
e.Measure(new Size(32.0, 32.0));
e.Arrange(new Rect(0.0, 0.0, 32.0, 32.0));
// cached for increased performance
e.CacheMode = new BitmapCache();
BitmapCacheBrush bcb = new BitmapCacheBrush(e);
DiffuseMaterial dm = new DiffuseMaterial(bcb);
component_model.Material = dm;
positions = new Point3DCollection();
triangles = new Int32Collection();
textures = new PointCollection();
((MeshGeometry3D)component_model.Geometry).Positions = positions;
((MeshGeometry3D)component_model.Geometry).TextureCoordinates = textures;
((MeshGeometry3D)component_model.Geometry).TriangleIndices = triangles;
}
public Model3D ComponentModel => component_model;
public void Update() {
if (p == null) return;
if (!is_done) {
int pos_index = id * 4;
// compute positions
positions.Add(new Point3D(p.position.X, p.position.Y, p.position.Z));
positions.Add(new Point3D(p.position.X, p.position.Y + p.size, p.position.Z));
positions.Add(new Point3D(p.position.X + p.size, p.position.Y + p.size, p.position.Z));
positions.Add(new Point3D(p.position.X + p.size, p.position.Y, p.position.Z));
// compute texture co-ordinates
textures.Add(new Point(0.0, 0.0));
textures.Add(new Point(0.0, 1.0));
textures.Add(new Point(1.0, 1.0));
textures.Add(new Point(1.0, 0.0));
// compute triangle indices
triangles.Add(pos_index);
triangles.Add(pos_index+2);
triangles.Add(pos_index+1);
triangles.Add(pos_index);
triangles.Add(pos_index+3);
triangles.Add(pos_index+2);
// commenting out line below enables rendering of components but v. slow
// due to continually filling up above collections
is_done = true;
}
}
public void SpawnParticle(Point3D _pos, double _size) {
p = new Particle {
position = _pos,
size = _size
};
}
public void Clear() {
((MeshGeometry3D)component_model.Geometry).Positions.Clear();
((MeshGeometry3D)component_model.Geometry).TextureCoordinates.Clear();
((MeshGeometry3D)component_model.Geometry).TriangleIndices.Clear();
}
}
MainWindow.xaml
The (crude) XAML code just for completeness in case anyone wants to verify this example.
<Window x:Class="GraphicsTestingWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:GraphicsTestingWPF"
mc:Ignorable="d"
Title="MainWindow" Height="768" Width="1366">
<Grid>
<Grid Background="Black" Visibility="Visible" Width ="Auto" Height="Auto" Margin="5,3,623,10" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Viewport3D Name="World" Focusable="True">
<Viewport3D.Camera>
<OrthographicCamera x:Name="orthograghic_camera" Position="0,0,32" LookDirection="0,0,-32" UpDirection="0,1,0" Width="256"/>
</Viewport3D.Camera>
<Viewport3D.Children>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup x:Name="WorldModels">
<AmbientLight Color="#FFFFFFFF" />
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D.Children>
</Viewport3D>
</Grid>
<Slider Maximum="1000" TickPlacement="BottomRight" TickFrequency="50" IsSnapToTickEnabled="True" x:Name="particles_slider" Margin="0,33,130,0" VerticalAlignment="Top" Height="25" HorizontalAlignment="Right" Width="337"/>
<Label x:Name="NParticles_Label" Content="Number of Particles" Margin="0,29,472,0" VerticalAlignment="Top" RenderTransformOrigin="1.019,-0.647" HorizontalAlignment="Right" Width="123"/>
<TextBox Text="{Binding ElementName=particles_slider, Path=Value, UpdateSourceTrigger=PropertyChanged}" x:Name="particle_val" Height="23" Margin="0,32,85,0" TextWrapping="Wrap" VerticalAlignment="Top" TextAlignment="Right" HorizontalAlignment="Right" Width="40"/>
<Button x:Name="generate_button" Content="Generate" Margin="0,86,520,0" VerticalAlignment="Top" Click="OnGenerateClick" HorizontalAlignment="Right" Width="75"/>
</Grid>
</Window>
Problem
As you may have surmised from the code, the issue lies in the Update methods of ComponentManager and Component. In order for the rendering to be successful I have to update each and every Component every time a particle is added to the system of particles - I tried to mitigate any performance issues from this by using the flag is_done in the class Component, to be set to true when the particle properties (positions, textures and triangles) were calculated the first time. Then, or so I thought, on each subsequent call to Component::Update() for a component the previously calculated values of these collections would be used.
However, this does not work here as setting is_done to true as explained above will simply cause nothing to be rendered. If I comment out is_done = true; then everything is rendered however it is incredibly slow - most likely due to the huge number of elements being added to the positions etc. collections of each Component (memory usage explodes as shown by the debugger diagnostics).
Question
Why do I have to keep adding previously calculated elements to these collections for rendering to occur?
In other words, why does it not just take the already calculated Positions, TextureCoordinates and TriangleIndices from each Component and use these when rendering?
It looks like there may be several problems here.
The first that I found was that you're calling comp_mgr.Update() every time you add a particle. This, in turn, calls Update() on every particle. All of that results in an O(n^2) operation which means that for 200 particles (your min), you're running the component update logic 40,000 times. This is definitely what's causing it to be slow.
To eliminate this, I moved the comp_mgr.Update() call out of the while loop. But then I got no points, just like when you uncomment the is_done = true; line.
Interestingly, when I added a second call to comp_mgr.Update(), I got a single point. And with successive calls I got an additional point with each call. This implies that, even with the slower code, you're still only getting 199 points on the 200-point setting.
There seems to be a deeper issue somewhere, but I'm unable to find it. I'll update if I do. Maybe this will lead you or someone else to the answer.
For now, the MainWindow.Generate() method looks like this:
private void Generate(int _total)
{
const int min = -75;
const int max = 75;
// generate particles
while (current_particles < _total)
{
int rand_x = rng.Next(min, max);
int rand_y = rng.Next(min, max);
comp_manager.AddParticleToComponent(new Point3D(rand_x, rand_y, .0), 1.0);
++current_particles;
}
Dispatcher.Invoke(() => { comp_manager.Update(); });
}
where replicating the Update() call n times results in n-1 points being rendered.
I have a WPF user control with which the user can draw a rectangle on the map canvas to define an area for downloading background imagery tiles from a web service. The tiles are in Lat long 1x1 degree.
This is working, I pass a point as a parameter & download the tile. However I am now attempting to pass a List<Point> for each corner of the user defined rectangle & therefore determine which tiles intersect each point. This works to an extent however if the user defines a rectangle completely within a single tile then the same tile is downloaded 4 times (once for each point):
ForEach(point in rectanglePointsList)
{
DownloadTile(point);
}
I need to iterate over the points & determine whether to download the subsequent tile or not. This code is dumb to the tiles, I only have the point parameters that I'm passing in. A colleague suggested a nested for loop whereby I convert the X & Y from each point, find the min & max & then somehow determine whether a tile should be downloaded knowing that the tiles are always 1x1 degree. Is there an algorithm to achieve this? Don't really know where to start.
List<int>xValuesList = new List<int>();
List<int> yValuesList = new List<int>();
ForEach(point in RectanglePointsList)
{
xValuesList.Add(Convert.ToInt32(point.X);
yValuesList.Add(Convert.ToInt32(point.Y);
}
int maxX = xValuesList.Select(value => value.X).Max();
int maxY = yValuesList.Select(value => value.Y).Max();
//Lost after here...
Here's a quick sample :D
selects a group of tiles
adds them or not to the cache
shows which have been added or not
supports a single mouse click, no need to draw a rectangle
encompasses selected tiles no matter where you start/end
Here I've clicked on the first twos, this time I've drawn a rectangle encompassing the twos below, they haven't been added twice.
XAML:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApplication1"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Name="Window"
Title="MainWindow"
Width="525"
Height="350"
Background="Transparent"
SnapsToDevicePixels="True"
UseLayoutRounding="True"
mc:Ignorable="d">
<Grid />
</Window>
Code:
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace WpfApplication1
{
public partial class MainWindow : Window
{
private readonly HashSet<Int32Point> _set = new HashSet<Int32Point>();
private readonly int columns = 10;
private readonly int rows = 10;
private bool _down;
private Point _position1;
private Point _position2;
private Size size = new Size(500, 500);
public MainWindow()
{
InitializeComponent();
MouseDown += MainWindow_MouseDown;
MouseMove += MainWindow_MouseMove;
MouseUp += MainWindow_MouseUp;
}
private void MainWindow_MouseUp(object sender, MouseButtonEventArgs e)
{
_down = false;
InvalidateVisual();
// find rects selected
var x1 = (int) Math.Floor(_position1.X/(size.Width/columns));
var y1 = (int) Math.Floor(_position1.Y/(size.Height/rows));
var x2 = (int) Math.Ceiling(_position2.X/(size.Width/columns));
var y2 = (int) Math.Ceiling(_position2.Y/(size.Height/rows));
var w = x2 - x1;
var h = y2 - y1;
var builder = new StringBuilder();
for (var y = 0; y < h; y++)
{
for (var x = 0; x < w; x++)
{
var int32Point = new Int32Point(x1 + x, y1 + y);
var add = _set.Add(int32Point);
if (add)
{
// download image !!!
}
else
{
// image already downloaded, do something !
}
builder.AppendLine(string.Format("{0} : {1}", int32Point, (add ? "added" : "ignored")));
}
}
MessageBox.Show(builder.ToString());
}
private void MainWindow_MouseMove(object sender, MouseEventArgs e)
{
_position2 = e.GetPosition(this);
InvalidateVisual();
}
private void MainWindow_MouseDown(object sender, MouseButtonEventArgs e)
{
_position1 = e.GetPosition(this);
_down = true;
}
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
InvalidateVisual();
}
protected override void OnRender(DrawingContext drawingContext)
{
// draw a mini-map
for (var y = 0; y < rows; y++)
{
for (var x = 0; x < columns; x++)
{
var color = Color.FromRgb((byte) ((double) x/columns*255), (byte) ((double) y/rows*255), 255);
var brush = new SolidColorBrush(color);
var w = size.Width/columns;
var h = size.Height/rows;
var rect = new Rect(w*x, h*y, w, h);
drawingContext.DrawRectangle(brush, null, rect);
}
}
// draw selection rectangle
if (_down)
{
drawingContext.DrawRectangle(null, new Pen(new SolidColorBrush(Colors.White), 2.0),
new Rect(_position1, _position2));
}
}
private struct Int32Point
{
public readonly int X, Y;
public Int32Point(int x, int y)
{
X = x;
Y = y;
}
public override string ToString()
{
return $"X: {X}, Y: {Y}";
}
}
}
}
Go on and improve on that !
I'm trying to create a simple Window that allows viewing/zooming/moving an image. It is starting to behave similar to what it should with the image resizing to adjust to the window as we resize.
The weird thing is that as you reduce the width of the window, at a certain point, the height of the picture starts reducing when it shouldn't, and then it bounces into a different position. In the code, however, the height of the image remains the same so "something" else is altering the layout.
The other weird thing is that as you increase the width, the image almost remains centered except that it gradually tilts towards the right.
So my question is: what is screwing up my layout like this?
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="NaturalGroundingPlayer.ImageViewerWindow"
x:Name="Window" Title="Image Viewer" Width="640" Height="480" SizeChanged="Window_SizeChanged">
<Grid x:Name="LayoutRoot">
<Canvas x:Name="ImgCanvas" ClipToBounds="True">
<ContentControl x:Name="ImgContentCtrl" Height="300" Width="400">
<Grid x:Name="ImgGrid">
<Image x:Name="ImgObject"/>
<Thumb x:Name="ImgThumb" Opacity="0" DragDelta="ImgThumb_DragDelta" MouseWheel="ImgThumb_MouseWheel"/>
</Grid>
</ContentControl>
</Canvas>
</Grid>
</Window>
public partial class ImageViewerWindow : Window {
public static ImageViewerWindow Instance(string fileName) {
ImageViewerWindow NewForm = new ImageViewerWindow();
NewForm.LoadImage(fileName);
NewForm.Show();
return NewForm;
}
private double scale;
public ImageViewerWindow() {
InitializeComponent();
}
public void LoadImage(string fileName) {
BitmapImage NewImage = new BitmapImage();
NewImage.BeginInit();
NewImage.UriSource = new Uri(fileName);
NewImage.EndInit();
ImgObject.Source = NewImage;
//ImgContentCtrl.Width = NewImage.Width;
//ImgContentCtrl.Height = NewImage.Height;
BestFit();
}
private void ImgThumb_DragDelta(object sender, DragDeltaEventArgs e) {
double ImgLeft = Canvas.GetLeft(ImgContentCtrl);
double ImgTop = Canvas.GetTop(ImgContentCtrl);
Canvas.SetLeft(ImgContentCtrl, (ImgLeft + e.HorizontalChange));
Canvas.SetTop(ImgContentCtrl, (ImgTop + e.VerticalChange));
}
private void ImgThumb_MouseWheel(object sender, MouseWheelEventArgs e) {
// Zoom in when the user scrolls the mouse wheel up and vice versa.
if (e.Delta > 0) {
// Limit zoom-in to 500%
if (scale < 5)
scale += 0.1;
} else {
// When mouse wheel is scrolled down...
// Limit zoom-out to 80%
if (scale > 0.8)
scale -= 0.1;
}
DisplayImage();
}
private void BestFit() {
// Set the scale of the ContentControl to 100%.
scale = 1;
// Set the position of the ContentControl so that the image is centered.
Canvas.SetLeft(ImgContentCtrl, 0);
Canvas.SetTop(ImgContentCtrl, 0);
}
private void Window_SizeChanged(object sender, SizeChangedEventArgs e) {
DisplayImage();
}
private void DisplayImage() {
double RatioWidth = LayoutRoot.ActualWidth / ImgObject.Source.Width;
double RatioHeight = LayoutRoot.ActualHeight / ImgObject.Source.Height;
if (RatioHeight > RatioWidth) {
ImgContentCtrl.Width = LayoutRoot.ActualWidth * scale;
ImgContentCtrl.Height = LayoutRoot.ActualHeight * RatioHeight * scale;
} else {
ImgContentCtrl.Height = LayoutRoot.ActualHeight * scale;
ImgContentCtrl.Width = LayoutRoot.ActualWidth * RatioWidth * scale;
}
}
}
Sjips, you first need to put any picture in it to display.
dkozl, that works indeed, I wasn't sure the Image control would respect proportions automatically as the demo I found wasn't respecting proportions while resizing. Simply putting this code in DisplayImage works perfect, even for repositioning properly when zooming. I guess I was much closer to the answer than I thought!
private void DisplayImage() {
ImgContentCtrl.Width = LayoutRoot.ActualWidth * scale;
Canvas.SetLeft(ImgContentCtrl, LayoutRoot.ActualWidth * (1 - scale) / 2);
ImgContentCtrl.Height = LayoutRoot.ActualHeight * scale;
Canvas.SetTop(ImgContentCtrl, LayoutRoot.ActualHeight * (1 - scale) / 2);
}
That being said, I still don't undestand the weird behaviors that happened before...