WPF - Draw multiple Rectangles on Canvas in parallel - c#

I have class BBox, which repesents Rectangle - it contains cooridinates(x, y, width, height) and color of rectangle. Then I have List of these BBoxes, which contains circa 4000 of them. I need to draw all boxes which are stored in a List on a Canvas as fast as possible. What is the most effective way?
private List<BBox> FoundBoxes { get; set; }
public void DrawBoxes(Canvas canvas)
{
foreach (var box in FoundBoxes)
{
var brush = box.getColor();
System.Windows.Shapes.Rectangle rect;
rect = new System.Windows.Shapes.Rectangle
{
Stroke = brush,
Height = box.Height,
Width = box.Width,
StrokeThickness = 1
};
Canvas.SetLeft(rect, box.TopLeftX);
Canvas.SetTop(rect, box.TopLeftY);
canvas.Children.Add(rect);
}
}
Code above takes more than 1 second, which is slow for my application. I am sure that there must be a way to do it in parallel. So I tried this:
Parallel.ForEach(FoundBoxes, box =>
{
...same method body...
});
but it throws
System.InvalidOperationException: 'The calling thread must be STA, because many UI components require this.'
I am aware why this exceprtion occurs, and I tried to find solution, but nothing works for me. For example I tried running it using Dispatcher.Invoke(() => or running it using new Thread as stated here:
Thread t = new Thread(delegate ()
{
Parallel.ForEach(FoundBoxes, box =>
{
...same method body...
});
});
t.SetApartmentState(ApartmentState.STA);
t.IsBackground = true;
t.Start();
but it still throws mentioned exception. How can I solve this? Or is there any better / more effective way to draw multiple objects on canvas? Thank you.

Allright I solved my problem using WriteableBitmapEx as Clemens recommended. Now, 4000+ boxes are drawn in real-time.

Related

How to update wireframe without WPF button click?

I'm developing a Windows Form application with WPF User Control embedded in the WF. If I add a button and execute my userControl.DrawWireFrameCube(); My ViewPort3D get updated. I'm using Helix 3D Toolkit. But If I call my method from my MainWindowForm class it doesn't get executed and UI is not updated,but only userControl.DrawWireFrameCube(); isn't working. The other userControl.Draw3DObject(insertionPoint, points, color); method is working fine.
private void VisualizePart(int index)
{
InsertionPoint insertionPoint = new InsertionPoint
{
X = _duplicatedListParts[index].INFO1,
Y = _duplicatedListParts[index].INFO2,
Z = _duplicatedListParts[index].INFO3
};
DetailSize points = new DetailSize
{
Length = _duplicatedListParts[index].LENGTH,
Width = _duplicatedListParts[index].WIDTH,
Thickness = _duplicatedListParts[index].THICKNESS
};
userControl.Dispatcher.Invoke(() =>
{
System.Windows.Media.Color color = System.Windows.Media.Color.FromRgb(255, 90, 0);
userControl.Draw3DObject(insertionPoint, points, color);
userControl.DrawWireFrameCube();
});
}
The difference is that in my Draw3DObject() I add items to Model3DGroup and in DrawWireFrameCube() I add items to MyViewPort3D. I'm not using XAML and I want to stay that way.
Any ideas what is wrong here?
P.S I love negative vote without explanation :)

Render Livecharts Chart in Memory and Save as Image

I'm trying to do the following:
Create a Livechart Cartesian chart in memory
Add the chart to a grid
Add Labels to the same grid
Add the grid to a Viewbox
Render the Viewbox as a PNG
Save the PNG to disk
The above should be run from a different thread in the background in order to allow UI reponsiveness.
However simple this may seem, I've been struggling to get a proper working solution. The following issues are relevant:
The Livechart (which is inside the Viewbox) takes time to render
Thus the chart needs to be given time to complete rendering before trying to save it as an image
I have found code which makes use of HwndSource, however it is not working all the time (works about 95% of the time). Without the HwndSource modification it NEVER works (always gets a chart with nothing on it)
Running the Run() function in a different UI thread does not work, as I get the following error message: WPF Dispatcher {“The calling thread cannot access this object because a different thread owns it.”}
So my questions are:
What is the right way to wait for the Livechart/Grid/ViewBox combination to finish rendering before saving it as an image? Maybe make use of the Loaded event? Note that I have tried to impelment it but cannot get it to work as I hit the 'threading' issue.
How can I run the entire process in a different UI thread?
See below for code
public void Run()
(
//Create Livechart which is a child of a Grid control
Grid gridChart = Charts.CreateChart();
//Creates a ViewBox control which has the grid as its child
Viewbox viewBox = WrapChart(gridChart,1400,700);
//Creates and saves the image
CreateAndSaveImage(viewBox ,path,name);
)
Below is the function which creates the Viewbox and add the grid as a child
public Viewbox viewBox WrapChart(Grid grid,int width,int height)
{
chart.grid.Width = width;
chart.grid.Height = height;
viewbox.Child = chart.grid;
viewbox.Width = width;
viewbox.Height = height;
viewbox.Measure(new System.Windows.Size(width, height));
viewbox.Arrange(new Rect(0, 0, width, height));
viewbox.UpdateLayout();
}
Function below creates and saves the image
public void CreateAndSaveImage(Viewbox viewbox,string folderPath,string fileName)
{
var x = HelperFunctions.GetImage(viewbox);
System.IO.FileStream stream = System.IO.File.Create(folderPath + fileName);
HelperFunctions.SaveAsPng(x, stream);
stream.Close();
}
The following code renders the viewbox to an image. Note that this is the only code that I could find which waits for the chart to finish loading. I have no idea how it works, but it works 95% of the time. Sometimes a chart still does not finish loading.
public static RenderTargetBitmap GetImage(Viewbox view)
{
using (new HwndSource(new HwndSourceParameters())
{
RootVisual =
(VisualTreeHelper.GetParent(view) == null
? view
: null)
})
{
Size size = new Size(view.ActualWidth, view.ActualHeight);
if (size.IsEmpty)
return null;
int actualWidth = Convert.ToInt32(size.Width);
int requiredWidth = Convert.ToInt32(size.Width * 1);
int actualHeight = Convert.ToInt32(size.Height);
int requiredHeight = Convert.ToInt32(size.Height * 1);
// Flush the dispatcher queue
view.Dispatcher.Invoke(DispatcherPriority.SystemIdle, new Action(() => { }));
var renderBitmap = new RenderTargetBitmap(requiredWidth, requiredHeight,
96d * requiredWidth / actualWidth, 96d * requiredHeight / actualHeight,
PixelFormats.Pbgra32);
DrawingVisual drawingvisual = new DrawingVisual();
using (DrawingContext context = drawingvisual.RenderOpen())
{
context.DrawRectangle(new VisualBrush(view), null, new Rect(new Point(), size));
context.Close();
}
renderBitmap.Render(view);
renderBitmap.Freeze();
return renderBitmap;
}
}
The following code saves the bitmap as a picture to file
public static void SaveAsPng(BitmapSource src, Stream outputStream)
{
PngBitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(src));
encoder.Save(outputStream);
}
The following code is what I use to run the entire thing in a different thread. Note that it is not working, as I get the following error message:
WPF Dispatcher {“The calling thread cannot access this object because
a different thread owns it.”}.
Note that if I execute Run() normally (without any separate threads) it works, however sometimes the chart does not render properly (as explained previously).
Thread thread = new Thread(() =>
{
Run();
System.Windows.Threading.Dispatcher.Run();
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
Try call this line for the chart:
this.chart.Model.Updater.Run(false, true);
This line updates the chart and always is visible when save to image.

How to ensure UWP app is always full screen on launch?

Is there a way (either C# or XAML) I can maximize a UWP app window even after I resized and closed it previously on desktop?
I have tried with ApplicationViewWindowingMode.FullScreen but this makes the app go entire full screen and covers the Windows Taskbar too.
You can use another value PreferredLaunchViewSize from ApplicationViewWindowingMode and then set ApplicationView.PreferredLaunchViewSize but the key is to find out what the size is going to be.
Theoretically, you could use a really big number and window would just extend to the max it could be. However, it's probably safer to just calculate the screen dimensions in effective pixels.
So if you just call the following method before InitializeComponent(); on your main Page, it should maximize the window on startup.
private static void MaximizeWindowOnLoad()
{
// Get how big the window can be in epx.
var bounds = ApplicationView.GetForCurrentView().VisibleBounds;
ApplicationView.PreferredLaunchViewSize = new Size(bounds.Width, bounds.Height);
ApplicationView.PreferredLaunchWindowingMode = ApplicationViewWindowingMode.PreferredLaunchViewSize;
}
Note the app somehow remembers these settings even after you uninstalled it. If you ever want to change back to the default behavior (app starts up with the previous window size), simply call ApplicationView.PreferredLaunchWindowingMode = ApplicationViewWindowingMode.Auto; once and remove all the code.
Update
Looks like in the latest Windows 10 build, ApplicationView.GetForCurrentView().VisibleBounds no longer returns the full window size in effective pixels anymore. So we now need a new way to calculate it.
Turns out it's quite straightforward since the DisplayInformation class also gives us the screen resolution as well as the scale factor.
The following is the updated code -
public MainPage()
{
MaximizeWindowOnLoad();
InitializeComponent();
void MaximizeWindowOnLoad()
{
var view = DisplayInformation.GetForCurrentView();
// Get the screen resolution (APIs available from 14393 onward).
var resolution = new Size(view.ScreenWidthInRawPixels, view.ScreenHeightInRawPixels);
// Calculate the screen size in effective pixels.
// Note the height of the Windows Taskbar is ignored here since the app will only be given the maxium available size.
var scale = view.ResolutionScale == ResolutionScale.Invalid ? 1 : view.RawPixelsPerViewPixel;
var bounds = new Size(resolution.Width / scale, resolution.Height / scale);
ApplicationView.PreferredLaunchViewSize = new Size(bounds.Width, bounds.Height);
ApplicationView.PreferredLaunchWindowingMode = ApplicationViewWindowingMode.PreferredLaunchViewSize;
}
}
If you want to MAXIMISE your app on launch you can use the following:
ApplicationView.PreferredLaunchWindowingMode = ApplicationViewWindowingMode.Maximized;
But be sure to put it into the Loaded Event for your Page or it will not work!
I've too few points to comment directly. None of the above resized to a maximized view for me (or the below single-line ApplicationViewWindowingMode.Maximized method), but I have used some of the answers to come up with something that worked for me. It is still very clunky however. The screen size given in 'DisplayInformation' is too big to allow the page to be resized directly to it. Trying to do it didn't work and I had to take 60 off height and width to get it to return 'true', therefore I have the following bit of nonsense which worked, maybe it will help someone else find a better answer. It goes in the page/window loaded event. Nothing else needs to be added elsewhere.
private void Page_Loaded(object sender, RoutedEventArgs e)
{
var view = ApplicationView.GetForCurrentView();
var displayInfo = DisplayInformation.GetForCurrentView();
double x = ActualWidth;
double y = ActualHeight;
bool answer = true;
// Get the screen resolution (APIs available from 14393 onward).
var resolution = new Size(displayInfo.ScreenWidthInRawPixels-60, displayInfo.ScreenHeightInRawPixels-60);
answer = view.TryResizeView(resolution); //This should return true if the resize is successful
if (answer)
{
x = displayInfo.ScreenWidthInRawPixels - 60;
y = displayInfo.ScreenHeightInRawPixels - 60;
}
answer = true;
while (answer == true)
{
x++;
answer = view.TryResizeView(new Size { Width = x, Height = y });
}
x = x - 1;
answer = true;
while (answer == true)
{
y++;
answer = view.TryResizeView(new Size { Width = x, Height = y });
}
Adding the following line to the OnLaunched event under App.xaml.cs did it for me.
ApplicationView.PreferredLaunchWindowingMode = ApplicationViewWindowingMode.FullScreen;
NOTE: Make sure to add it before the following line
Window.Current.Activate();
If you like to go fullscreen at the runtime use the following line.
ApplicationView.GetForCurrentView().TryEnterFullScreenMode();
I have this one liner that works as I expected Justins code to, but for some reason, when using Justins answer, my window would not be maximized... But then I changed something that did make it maximized but I lost all my fluent design such as Acrylic and RevealHighlite...
So I came up with this one liner which keeps all of my fluent design principles happy:
ApplicationView.GetForCurrentView().TryEnterFullScreenMode();
Something to note:
I did try Justins answer, and I am using his method of MaximizeWindowOnLoad() which I have called straight after the initializeComponent();
Full overview:
public class()
{
this.InitializeComponent();
MaximizeWindowOnLoad();
}
private static void MaximizeWindowOnLoad()
{
ApplicationView.GetForCurrentView().TryEnterFullScreenMode();
}

Whole line highlighting in AvalonEdit

I'm creating a view for patch files using AvalonEdit, and I want to make it so that diffs are highlighted across the entire line, not just the text background - similar to what GitHub for Windows does today:
I'm new to AvalonEdit, so I'm not sure the best way to do this. Here's what I've found so far:
Override VisualLineElementGenerator in order to create an additional TextSpan that is the length of the control. This seems Tricky.
Create a new control to add to TextView.Layers in the background and OnRender in the green/red by hand - this seems more promising, but it's not super clear what event I need to hook in order to detect when to re-render.
Override TextView - this seems like overkill.
Edit: Here's what happens with a simple syntax highlighter, which is what I don't want:
As Daniel mentioned, I discovered Background Renderers via the Wiki page and it worked great. Here's what I ended up doing - it can probably be made a bit more efficient but it's good enough for now:
public class DiffLineBackgroundRenderer : IBackgroundRenderer
{
static Pen pen;
static SolidColorBrush removedBackground;
static SolidColorBrush addedBackground;
static SolidColorBrush headerBackground;
FileDiffView host;
static DiffLineBackgroundRenderer()
{
removedBackground = new SolidColorBrush(Color.FromRgb(0xff, 0xdd, 0xdd)); removedBackground.Freeze();
addedBackground = new SolidColorBrush(Color.FromRgb(0xdd, 0xff, 0xdd)); addedBackground.Freeze();
headerBackground = new SolidColorBrush(Color.FromRgb(0xf8, 0xf8, 0xff)); headerBackground.Freeze();
var blackBrush = new SolidColorBrush(Color.FromRgb(0, 0, 0)); blackBrush.Freeze();
pen = new Pen(blackBrush, 0.0);
}
public DiffLineBackgroundRenderer(FileDiffView host)
{
this.host = host;
}
public KnownLayer Layer
{
get { return KnownLayer.Background; }
}
public void Draw(TextView textView, DrawingContext drawingContext)
{
foreach (var v in textView.VisualLines)
{
var rc = BackgroundGeometryBuilder.GetRectsFromVisualSegment(textView, v, 0, 1000).First();
// NB: This lookup to fetch the doc line number isn't great, we could
// probably do it once then just increment.
var linenum = v.FirstDocumentLine.LineNumber - 1;
if (linenum >= host.ViewModel.Lines.Count) continue;
var diffLine = host.ViewModel.Lines[linenum];
if (diffLine.Style == DiffLineStyle.Context) continue;
var brush = default(Brush);
switch (diffLine.Style)
{
case DiffLineStyle.Header:
brush = headerBackground;
break;
case DiffLineStyle.Added:
brush = addedBackground;
break;
case DiffLineStyle.Deleted:
brush = removedBackground;
break;
}
drawingContext.DrawRectangle(brush, pen,
new Rect(0, rc.Top, textView.ActualWidth, rc.Height));
}
}
}
Create a new control to add to TextView.Layers in the background and OnRender in the green/red by hand
You don't have to create a new layer to render in something in the background: you can add your implementation of IBackgroundRenderer to textView.BackgroundRenderers to render something in the background of an existing layer.
it's not super clear what event I need to hook in order to detect when to re-render
That would be: textView.VisualLinesChanged. But you don't need that if you use IBackgroundRenderer as the existing layers are already re-rendered by AvalonEdit.

C#: Windows Forms: What could cause Invalidate() to not redraw?

I'm using Windows Forms. For a long time, pictureBox.Invalidate(); worked to make the screen be redrawn. However, it now doesn't work and I'm not sure why.
this.worldBox = new System.Windows.Forms.PictureBox();
this.worldBox.BackColor = System.Drawing.SystemColors.Control;
this.worldBox.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.worldBox.Location = new System.Drawing.Point(170, 82);
this.worldBox.Name = "worldBox";
this.worldBox.Size = new System.Drawing.Size(261, 250);
this.worldBox.TabIndex = 0;
this.worldBox.TabStop = false;
this.worldBox.MouseMove += new
System.Windows.Forms.MouseEventHandler(this.worldBox_MouseMove);
this.worldBox.MouseDown += new
System.Windows.Forms.MouseEventHandler(this.worldBox_MouseDown);
this.worldBox.MouseUp += new
System.Windows.Forms.MouseEventHandler(this.worldBox_MouseUp);
Called in my code to draw the world appropriately:
view.DrawWorldBox(worldBox, canvas, gameEngine.GameObjectManager.Controllers,
selectedGameObjects, LevelEditorUtils.PREVIEWS);
View.DrawWorldBox:
public void DrawWorldBox(PictureBox worldBox,
Panel canvas,
ICollection<IGameObjectController> controllers,
ICollection<IGameObjectController> selectedGameObjects,
IDictionary<string, Image> previews)
{
int left = Math.Abs(worldBox.Location.X);
int top = Math.Abs(worldBox.Location.Y);
Rectangle screenRect = new Rectangle(left, top, canvas.Width,
canvas.Height);
IDictionary<float, ICollection<IGameObjectController>> layers =
LevelEditorUtils.LayersOfControllers(controllers);
IOrderedEnumerable<KeyValuePair<float,
ICollection<IGameObjectController>>> sortedLayers
= from item in layers
orderby item.Key descending
select item;
using (Graphics g = Graphics.FromImage(worldBox.Image))
{
foreach (KeyValuePair<float, ICollection<IGameObjectController>>
kv in sortedLayers)
{
foreach (IGameObjectController controller in kv.Value)
{
// ...
float scale = controller.View.Scale;
float width = controller.View.Width;
float height = controller.View.Height;
Rectangle controllerRect = new
Rectangle((int)controller.Model.Position.X,
(int)controller.Model.Position.Y,
(int)(width * scale),
(int)(height * scale));
// cull objects that aren't intersecting with the canvas
if (controllerRect.IntersectsWith(screenRect))
{
Image img = previews[controller.Model.HumanReadableName];
g.DrawImage(img, controllerRect);
}
if (selectedGameObjects.Contains(controller))
{
selectionRectangles.Add(controllerRect);
}
}
}
foreach (Rectangle rect in selectionRectangles)
{
g.DrawRectangle(drawingPen, rect);
}
selectionRectangles.Clear();
}
worldBox.Invalidate();
}
What could I be doing wrong here?
To understand this you have to have some understanding of the way this works at the OS level.
Windows controls are drawn in response to a WM_PAINT message. When they receive this message, they draw whichever part of themselves has been invalidated. Specific controls can be invalidated, and specific regions of controls can be invalidated, this is all done to minimize the amount of repainting that's done.
Eventually, Windows will see that some controls need repainting and issue WM_PAINT messages to them. But it only does this after all other messages have been processed, which means that Invalidate does not force an immediate redraw. Refresh technically should, but isn't always reliable. (UPDATE: This is because Refresh is virtual and there are certain controls in the wild that override this method with an incorrect implementation.)
There is one method that does force an immediate paint by issuing a WM_PAINT message, and that is Control.Update. So if you want to force an immediate redraw, you use:
control.Invalidate();
control.Update();
This will always redraw the control, no matter what else is happening, even if the UI is still processing messages. Literally, I believe it uses the SendMessage API instead of PostMessage which forces painting to be done synchronously instead of tossing it at the end of a long message queue.
Invalidate() only "invalidates" the control or form (marks it for repainting), but does not force a redraw. It will be redrawn as soon as the application gets around to repainting again when there are no more messages to process in the message queue. If you want to force a repaint, you can use Refresh().
Invalidate or Refresh will do the same thing in this case, and force a redraw (eventually). If you're not seeing anything redrawn (ever), then that means either nothing has been drawn at all in DrawWorldBox or whatever has been drawn has been drawn off the visible part of the PictureBox's image.
Make sure (using breakpoints or logging or stepping through the code, as you prefer) that something is being is being added to selectionRectangles, and that at least one of those rectangles covers the visible part of the PictureBox. Also make sure the pen you're drawing with doesn't happen to be the same color as the background.

Categories

Resources