I have drawn an ellipse using an EllipsePoints array which defines the height width and color of the ellipse.
Then using a for loop to get the position of the ellipse using the ellipse points and a random number to set its position:
Random rand = new Random();
Int32 randomNumber = rand.Next(0, 310);
Int32 randomNumber2 = rand.Next(0, 500);
for (int j = 0; j < 60; j++)
{
ellipsePoints[j] = new Ellipse() { Width = 20, Height = 20, Fill = Brushes.Red };
canvas1.Children.Add(ellipsePoints[j]);
}
for (int i = 0; i < 60; i++)
{
Canvas.SetLeft(ellipsePoints[i], randomNumber2);
Canvas.SetTop(ellipsePoints[i], randomNumber);
}
What could I do to make the ellipse vanish after a certain amount of time and then appear in another random location?
There are 2 important aspects to this question.
Timer - In WPF we use the System.Windows.Threading.DispatcherTimer.
Remove elements- One way is to maintain a copy of the UI element before adding it to the Canvas. I have saved it in a class variable so that I can later remove it from the canvas using the following method
PaintCanvas.Children.Remove(ellipse);
Create you WPF and add a canvas called PaintCanvas
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="889" Width="1080">
<Canvas Name="PaintCanvas">
<Button Canvas.Left="46" Canvas.Top="274" Content="Button" Height="23" Name="button1" Width="75" Click="button1_Click" />
</Canvas >
</Window>
The Code. I have documented it.
public partial class MainWindow : Window
{
int loopCounter;
private System.Windows.Threading.DispatcherTimer timer;
Random rand = new Random();
Ellipse ellipse = null;
public MainWindow()
{
InitializeComponent();
//Initialize the timer class
timer = new System.Windows.Threading.DispatcherTimer();
timer.Interval = TimeSpan.FromSeconds(1); //Set the interval period here.
timer.Tick += timer1_Tick;
}
private void button1_Click(object sender, RoutedEventArgs e)
{
loopCounter = 10;
timer.Start();
}
private void timer1_Tick(object sender, EventArgs e)
{
//Remove the previous ellipse from the paint canvas.
PaintCanvas.Children.Remove(ellipse);
if (--loopCounter == 0)
timer.Stop();
//Add the ellipse to the canvas
ellipse=CreateAnEllipse(20,20 );
PaintCanvas.Children.Add(ellipse);
Canvas.SetLeft(ellipse, rand.Next(0, 310));
Canvas.SetTop(ellipse, rand.Next(0, 500));
}
// Customize your ellipse in this method
public Ellipse CreateAnEllipse(int height,int width)
{
SolidColorBrush fillBrush = new SolidColorBrush() { Color = Colors.Red };
SolidColorBrush borderBrush = new SolidColorBrush() { Color = Colors.Black };
return new Ellipse()
{
Height = height,
Width = width,
StrokeThickness = 1,
Stroke = borderBrush,
Fill = fillBrush
};
}
}
Related
This code for a WPF works for one program. It is not working in a new program being created. It results in an exception --- "System.InvalidOperationException: 'Specified element is already the logical child of another element. Disconnect it first.'"
This is part of the .xaml code:
<Canvas x:Name="gCanvasPlotTop"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="0,0,0,0"
Width="500"
Height="150" />
<Canvas x:Name="gCanvasPlotBottom"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="0,0,0,0"
Width="500"
Height="150" />
Below is part of the .cs code.
Before I call dlgDisplayTwoXYPlots(), I define poXY:
Polyline poXYTop = new Polyline { Stroke = Brushes.Blue };
Polyline poXYBottom = new Polyline { Stroke = Brushes.Blue };
//------------------------------
public dlgDisplayTwoXYPlots(List<double> listdParamsTop,
List<Point> listPointsTop,
Polyline poXYTop,
List<double> listdParamsBottom,
List<Point> listPointsBottom,
Polyline poXYBottom)
{
InitializeComponent();
glistdParamsTop = listdParamsTop;
glistPointsTop = listPointsTop;
gpoXYTop = poXYTop;
glistdParamsBottom = listdParamsBottom;
glistPointsBottom = listPointsBottom;
gpoXYBottom = poXYBottom;
}//DlgPlotXY()
//------------------------------
//------------------------------
private void Window_Loaded(object sender, RoutedEventArgs e)
{
Plot(gCanvasPlotTop, gpoXYTop, glistdParamsTop, glistPointsTop);
Plot(gCanvasPlotBottom, gpoXYBottom, glistdParamsBottom, glistPointsBottom);
}//Window_Loaded
//------------------------------
//------------------------------
private void Plot(Canvas canvas, Polyline poXY, List<double> listdParams, List<Point> listPoints)
{
int iii = 0;
int iNumOfPoints = (int)listdParams[iii++];
double dXmin = listdParams[iii++];
double dXmax = listdParams[iii++];
double dYmin = listdParams[iii++];
double dYmax = listdParams[iii++];
double dPlotWidth = dXmax - dXmin;
double dPlotHeight = dYmax - dYmin;
for (int ii = 0; ii < iNumOfPoints; ii++) {
var pointResult = new Point {
X = (listPoints[ii].X - dXmin) * canvas.Width / dPlotWidth,
Y = canvas.Height - (listPoints[ii].Y - dYmin) * canvas.Height / dPlotHeight
};
poXY.Points.Add(pointResult);
}
canvas.Children.Add(poXY);
//------------------------------
Why does a WPF canvas result in System.InvalidOperationException?
In MainWindow I had
Polyline poXYTop = new Polyline { Stroke = Brushes.Blue};
Polyline poXYBottom = new Polyline { Stroke = Brushes.Blue};
OnePlot(listdSignalXValues, listdSignalYValues, poXYTop, "Signal");
TwoPlots(listdSignalXValues, listdSignalYValues, poXYTop, "Signal DupTop",
listdSignalXValues, listdSignalYValues, poXYBottom, "Signal Bottom");
I had assumed that poXYTop would not return poXYTop with additional info as it was not a ref poXYTop. Now I know it does.
This works:
Polyline poXY = new Polyline { Stroke = Brushes.Blue };
Polyline poXYTop = new Polyline { Stroke = Brushes.Blue };
Polyline poXYBottom = new Polyline { Stroke = Brushes.Blue };
OnePlot(listdSignalXValues, listdSignalYValues, poXY, "Signal");
TwoPlots(listdSignalXValues, listdSignalYValues, poXYTop, "Signal DupTop",
listdSignalXValues, listdSignalYValues, poXYBottom, "Signal Bottom");
I have a simple WPF application which is a window application. There is a Canvas on this window. What I want to do is when I move the mouse on the Canvas It should draw a rectangle on the canvas and next when I press the left mouse button the color of the rectangle should get changed. I am perfectly able to draw a rectangle on mouse move event and also receiving MouseDown event on Rectangle but when I am trying to change the color of this rectangle It is not working. The code is very simple
Here is my xaml file
<Window x:Class="WpfApp1.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:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Canvas Background="#11FFFFFF" IsHitTestVisible="True" x:Name="overlay" Opacity="1">
</Canvas>
</Grid>
</Window>
and here is my xaml.cs file
namespace WpfApp1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
overlay.MouseMove += OnOverlayMouseMove;
}
private void OnOverlayMouseMove(object sender, MouseEventArgs args)
{
overlay.Children.Clear();
Point ps = args.GetPosition(overlay);
Rectangle rect = new Rectangle
{
Fill = Brushes.LightBlue,
Stroke = Brushes.LightGray,
StrokeThickness = 2,
Width = 100,
Height = 50
};
rect.Opacity = 0.5;
rect.MouseLeftButtonDown += OnRectLeftMouseButtonDown;
rect.Name = "Blue";
Canvas.SetLeft(rect, ps.X - 50);
Canvas.SetTop(rect, ps.Y - 25);
overlay.Children.Add(rect);
}
private void OnRectLeftMouseButtonDown(object sender, MouseEventArgs args)
{
Rectangle rect = sender as Rectangle;
if (rect.Name.Equals("Blue"))
{
rect.Fill = Brushes.Black;
rect.Name = "Black";
}
else
{
rect.Fill = Brushes.LightBlue;
rect.Name = "Blue";
}
args.Handled = true;
}
}
}
The color is not changing because you are creating a new rectangle every time OnOverlayMouseMove is called and setting Fill to LightBlue
You can do something like the following,
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
overlay.MouseMove += OnOverlayMouseMove;
}
private void OnOverlayMouseMove(object sender, MouseEventArgs args)
{
overlay.Children.Clear();
Point ps = args.GetPosition(overlay);
Rectangle rect = new Rectangle
{
Fill = brush,
Stroke = Brushes.LightGray,
StrokeThickness = 2,
Width = 100,
Height = 50
};
rect.Opacity = 0.5;
rect.MouseLeftButtonDown += OnRectLeftMouseButtonDown;
rect.Name = "Blue";
Canvas.SetLeft(rect, ps.X - 50);
Canvas.SetTop(rect, ps.Y - 25);
overlay.Children.Add(rect);
}
private SolidColorBrush brush = Brushes.LightBlue;
private void OnRectLeftMouseButtonDown(object sender, MouseEventArgs args)
{
Rectangle rect = sender as Rectangle;
if (brush == Brushes.LightBlue)
{
brush = Brushes.Black;
}
else
{
brush = Brushes.LightBlue;
}
args.Handled = true;
}
}
I think you need something like this:
public partial class MainWindow : Window
{
private Color normal = Color.FromRgb(255, 0, 0);
private Color active = Color.FromRgb(0, 0, 0);
private SolidColorBrush rectangleBrush;
public MainWindow()
{
InitializeComponent();
rectangleBrush = new SolidColorBrush(normal);
overlay.MouseMove += OnOverlayMouseMove;
}
private void OnOverlayMouseMove(object sender, MouseEventArgs args)
{
overlay.Children.Clear();
Point ps = args.GetPosition(overlay);
Rectangle rect = new Rectangle
{
Fill = rectangleBrush,
Stroke = Brushes.LightGray,
StrokeThickness = 2,
Width = 100,
Height = 50
};
rect.Opacity = 0.5;
rect.MouseLeftButtonDown += OnRectLeftMouseButtonDown;
Canvas.SetLeft(rect, ps.X - 50);
Canvas.SetTop(rect, ps.Y - 25);
overlay.Children.Add(rect);
}
private void OnRectLeftMouseButtonDown(object sender, MouseEventArgs args)
{
Rectangle rect = sender as Rectangle;
if ((rect.Fill as SolidColorBrush).Color == normal) {
rectangleBrush.Color = active;
} else {
rectangleBrush.Color = normal;
}
args.Handled = true;
}
}
If you want use colors from Color struct just replace normal and active vars with "Colors.LightBlue" and "Colors.Black"
I am trying to simulate a LED display board with c# . I need a control which contains 1536 clickable controls to simulate LEDs (96 in width and 16 in Height). I used a panel named pnlContainer for this and user will add 1536 tiny customized panels at runtime. These customized panels should change their color by click event at runtime. Everything works . But adding this number of tiny panels to the container takes long time ( about 10 secs). What is your suggestion to solve this issue? Any tips are appreciated.
this is my custome panel:
public partial class LedPanel : Panel
{
public LedPanel()
{
InitializeComponent();
}
protected override void OnPaint(PaintEventArgs pe)
{
base.OnPaint(pe);
}
protected override void OnMouseDown(MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
if (this.BackColor == Color.Black)
{
this.BackColor = Color.Red;
}
else
{
this.BackColor = Color.Black;
}
}
}
}
and this is piece of code which adds tiny panels to the pnlContainer :
private void getPixels(Bitmap img2)
{
pnlContainer.Controls.Clear();
for (int i = 0; i < 96; i++)
{
for (int j = 0; j < 16; j++)
{
Custom_Controls.LedPanel led = new Custom_Controls.LedPanel();
led.Name = i.ToString() + j.ToString();
int lWidth = (int)(pnlContainer.Width / 96);
led.Left = i * lWidth;
led.Top = j * lWidth;
led.Width = led.Height = lWidth;
if (img2.GetPixel(i, j).R>numClear.Value)
{
led.BackColor = Color.Red;
}
else
{
led.BackColor = Color.Black;
}
led.BorderStyle = BorderStyle.FixedSingle;
pnlContainer.Controls.Add(led);
}
}
}
Is there any better approach or better control instead of panelto do this?
I agree with what #TaW recommends. Don't put 1000+ controls on a form. Use some sort of data structure, like an array to keep track of which LEDs need to be lit and then draw them in the Paint event of a Panel.
Here's an example. Put a Panel on a form and name it ledPanel. Then use code similar to the following. I just randomly set the values of the boolean array. You would need to set them appropriately in response to a click of the mouse. I didn't include that code, but basically you need to take the location of the mouse click, determine which array entry needs to be set (or unset) and then invalidate the panel so it will redraw itself.
public partial class Form1 : Form
{
//set these variables appropriately
int matrixWidth = 96;
int matrixHeight = 16;
//An array to hold which LEDs must be lit
bool[,] ledMatrix = null;
//Used to randomly populate the LED array
Random rnd = new Random();
public Form1()
{
InitializeComponent();
ledPanel.BackColor = Color.Black;
ledPanel.Resize += LedPanel_Resize;
//clear the array by initializing a new one
ledMatrix = new bool[matrixWidth, matrixHeight];
//Force the panel to repaint itself
ledPanel.Invalidate();
}
private void LedPanel_Resize(object sender, EventArgs e)
{
//If the panel resizes, then repaint.
ledPanel.Invalidate();
}
private void button1_Click(object sender, EventArgs e)
{
//clear the array by initializing a new one
ledMatrix = new bool[matrixWidth, matrixHeight];
//Randomly set 250 of the 'LEDs';
for (int i = 0; i < 250; i++)
{
ledMatrix[rnd.Next(0, matrixWidth), rnd.Next(0, matrixHeight)] = true;
}
//Make the panel repaint itself
ledPanel.Invalidate();
}
private void ledPanel_Paint(object sender, PaintEventArgs e)
{
//Calculate the width and height of each LED based on the panel width
//and height and allowing for a line between each LED
int cellWidth = (ledPanel.Width - 1) / (matrixWidth + 1);
int cellHeight = (ledPanel.Height - 1) / (matrixHeight + 1);
//Loop through the boolean array and draw a filled rectangle
//for each one that is set to true
for (int i = 0; i < matrixWidth; i++)
{
for (int j = 0; j < matrixHeight; j++)
{
if (ledMatrix != null)
{
//I created a custom brush here for the 'off' LEDs because none
//of the built in colors were dark enough for me. I created it
//in a using block because custom brushes need to be disposed.
using (var b = new SolidBrush(Color.FromArgb(64, 0, 0)))
{
//Determine which brush to use depending on if the LED is lit
Brush ledBrush = ledMatrix[i, j] ? Brushes.Red : b;
//Calculate the top left corner of the rectangle to draw
var x = (i * (cellWidth + 1)) + 1;
var y = (j * (cellHeight + 1) + 1);
//Draw a filled rectangle
e.Graphics.FillRectangle(ledBrush, x, y, cellWidth, cellHeight);
}
}
}
}
}
private void ledPanel_MouseUp(object sender, MouseEventArgs e)
{
//Get the cell width and height
int cellWidth = (ledPanel.Width - 1) / (matrixWidth + 1);
int cellHeight = (ledPanel.Height - 1) / (matrixHeight + 1);
//Calculate which LED needs to be turned on or off
int x = e.Location.X / (cellWidth + 1);
int y = e.Location.Y / (cellHeight + 1);
//Toggle that LED. If it's off, then turn it on and if it's on,
//turn it off
ledMatrix[x, y] = !ledMatrix[x, y];
//Force the panel to update itself.
ledPanel.Invalidate();
}
}
I'm sure there can be many improvements to this code, but it should give you an idea on how to do it.
#Chris and #user10112654 are right.
here is a code similar to #Chris but isolates the displaying logic in a separate class. (#Chris answered your question when I was writing the code :))))
just create a 2D array to initialize the class and pass it to the Initialize method.
public class LedDisplayer
{
public LedDisplayer(Control control)
{
_control = control;
_control.MouseDown += MouseDown;
_control.Paint += Control_Paint;
// width and height of your tiny boxes
_width = 5;
_height = 5;
// margin between tiny boxes
_margin = 1;
}
private readonly Control _control;
private readonly int _width;
private readonly int _height;
private readonly int _margin;
private bool[,] _values;
// call this method first of all to initialize the Displayer
public void Initialize(bool[,] values)
{
_values = values;
_control.Invalidate();
}
private void MouseDown(object sender, MouseEventArgs e)
{
var firstIndex = e.X / OuterWidth();
var secondIndex = e.Y / OuterHeight();
_values[firstIndex, secondIndex] = !_values[firstIndex, secondIndex];
_control.Invalidate(); // you can use other overloads of Invalidate method for the blink problem
}
private void Control_Paint(object sender, PaintEventArgs e)
{
if (_values == null)
return;
e.Graphics.Clear(_control.BackColor);
for (int i = 0; i < _values.GetLength(0); i++)
for (int j = 0; j < _values.GetLength(1); j++)
Rectangle(i, j).Paint(e.Graphics);
}
private RectangleInfo Rectangle(int firstIndex, int secondIndex)
{
var x = firstIndex * OuterWidth();
var y = secondIndex * OuterHeight();
var rectangle = new Rectangle(x, y, _width, _height);
if (_values[firstIndex, secondIndex])
return new RectangleInfo(rectangle, Brushes.Red);
return new RectangleInfo(rectangle, Brushes.Black);
}
private int OuterWidth()
{
return _width + _margin;
}
private int OuterHeight()
{
return _height + _margin;
}
}
public class RectangleInfo
{
public RectangleInfo(Rectangle rectangle, Brush brush)
{
Rectangle = rectangle;
Brush = brush;
}
public Rectangle Rectangle { get; }
public Brush Brush { get; }
public void Paint(Graphics graphics)
{
graphics.FillRectangle(Brush, Rectangle);
}
}
this is how it's used in the form:
private void button2_Click(object sender, EventArgs e)
{
// define the displayer class
var displayer = new LedDisplayer(panel1);
// define the array to initilize the displayer
var display = new bool[,]
{
{true, false, false, true },
{false, true, false, false },
{false, false, true, false },
{true, false, false, false }
};
// and finally
displayer.Initialize(display);
}
I am very new to C# and WPF and would to create a WPF application that draws shapes with a button. The shapes then need to be able to move around the canvas. When I create a shape in the XAML it moves. However I cannot get the one created by the button to move. Could anyone please assist? Below are the XAML and code that i am using.
XAML:
<Window x:Class="All_test.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Canvas x:Name="canvas" >
<Button Content="Button" Canvas.Left="250" Canvas.Top="260" Width="75" Click="Button_Click_1" />
<Rectangle x:Name="rect"
Height="100" Width ="100" Fill="red"
MouseLeftButtonDown="rect_MouseLeftButtonDown"
MouseLeftButtonUp="rect_MouseLeftButtonUp"
MouseMove="rect_MouseMove"
Canvas.Left="342" Canvas.Top="110" />
</Canvas>
This is the code I am using to move the red square drawn in XAML. How can I do the same for the green one created by the button?
public partial class MainWindow : Window
{
private bool _isRectDragInProg;
public MainWindow()
{
InitializeComponent();
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
Rectangle rect = new Rectangle();
rect.Fill = new SolidColorBrush(Colors.Green);
rect.Stroke = new SolidColorBrush(Colors.Black);
rect.Height = 100;
rect.Width = 100;
rect.StrokeThickness = 4;
canvas.Children.Add(rect);
InitializeComponent();
}
private void rect_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_isRectDragInProg = true;
rect.CaptureMouse();
}
private void rect_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
_isRectDragInProg = false;
rect.ReleaseMouseCapture();
}
private void rect_MouseMove(object sender, MouseEventArgs e)
{
if (!_isRectDragInProg) return;
// get the position of the mouse relative to the Canvas
var mousePos = e.GetPosition(canvas);
// center the rect on the mouse
double left = mousePos.X - (rect.ActualWidth / 2);
double top = mousePos.Y - (rect.ActualHeight / 2);
Canvas.SetLeft(rect, left);
Canvas.SetTop(rect, top);
}
You should bind the mouse events for this Rectangle:
private void Button_Click_1(object sender, RoutedEventArgs e)
{
Rectangle rect = new Rectangle();
rect.Fill = new SolidColorBrush(Colors.Green);
rect.Stroke = new SolidColorBrush(Colors.Black);
rect.Height = 100;
rect.Width = 100;
rect.StrokeThickness = 4;
// here
rect.MouseLeftButtonDown += rect_MouseLeftButtonDown;
rect.MouseLeftButtonUp += rect_MouseLeftButtonUp;
rect.MouseMove += rect_MouseMove;
canvas.Children.Add(rect);
// InitializeComponent(); <--- lose the InitializeComponent here, that's should only be called ones.. (in the constructor)
}
Recommendations:
Use the sender parameter to get the current Rectangle.
You can lose the _isRectDragInProg boolean... use the IsMouseCaptured property instead.
For example:
private void rect_MouseMove(object sender, MouseEventArgs e)
{
var rect = (Rectangle)sender;
if (!rect.IsMouseCaptured) return;
// get the position of the mouse relative to the Canvas
var mousePos = e.GetPosition(canvas);
// center the rect on the mouse
double left = mousePos.X - (rect.ActualWidth / 2);
double top = mousePos.Y - (rect.ActualHeight / 2);
Canvas.SetLeft(rect, left);
Canvas.SetTop(rect, top);
}
I would like to create a user control in WPF, which would be able draw an n x m matrix of characters using a mono-space font.
The control will accept a string[] as fast as possible ( 60 fps is the target) and draw it on the screen.
I need performance similar to mplayer ascii playback.
All characters are drawn using the same mono-space font, but may have different colors and background according to certain rules (similar to syntax highlighting in VS).
I have implemented the solution in C# WinForms without any problem and got to 60 FPS, but when I wanted to learn how to do this in WPF I only found several articles and posts describing problems with WPF performance and conflicting information.
So what is the best way do achieve highest performance in this case?
A naive approach I tried is:
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
Random rand = new Random();
public MainWindow()
{
InitializeComponent();
DispatcherTimer timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromMilliseconds(1);
timer.Tick += timer_Tick;
timer.Start();
}
string GenerateRandomString(int length)
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++)
{
sb.Append(rand.Next(10));
}
return sb.ToString();
}
void timer_Tick(object sender, EventArgs e)
{
myTextBlock.Inlines.Clear();
for (int i = 0; i < 30; i++)
{
var run = new Run();
run.Text = GenerateRandomString(800);
run.Foreground = new SolidColorBrush(Color.FromArgb((byte)rand.Next(256),(byte)rand.Next(256),(byte)rand.Next(256),(byte)rand.Next(256)));
run.Background = new SolidColorBrush(Color.FromArgb((byte)rand.Next(256),(byte)rand.Next(256),(byte)rand.Next(256),(byte)rand.Next(256)));
myTextBlock.Inlines.Add(run);
}
}
}
The question is: Can you do better than that in WPF?
P.S.
Yes, I could use DirectX directly, but this question is about WPF and not DX.
Probably the fastest way to draw text in WPF is to use GlyphRun.
Here is a sample code:
MainWindow.xaml
<Window x:Class="WpfApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="300" Width="300">
<Image>
<Image.Source>
<DrawingImage x:Name="drawingImage"/>
</Image.Source>
</Image>
</Window>
MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows;
using System.Windows.Media;
using System.Windows.Threading;
namespace WpfApplication
{
public partial class MainWindow : Window
{
Random rand = new Random();
Stopwatch stopwatch;
long frameCounter = 0;
GlyphTypeface glyphTypeface;
double renderingEmSize, advanceWidth, advanceHeight;
Point baselineOrigin;
public MainWindow()
{
InitializeComponent();
new Typeface("Consolas").TryGetGlyphTypeface(out this.glyphTypeface);
this.renderingEmSize = 10;
this.advanceWidth = this.glyphTypeface.AdvanceWidths[0] * this.renderingEmSize;
this.advanceHeight = this.glyphTypeface.Height * this.renderingEmSize;
this.baselineOrigin = new Point(0, this.glyphTypeface.Baseline * this.renderingEmSize);
CompositionTarget.Rendering += CompositionTarget_Rendering;
DispatcherTimer timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromMilliseconds(1000);
timer.Tick += timer_Tick;
timer.Start();
}
void CompositionTarget_Rendering(object sender, EventArgs e)
{
if (this.stopwatch == null)
this.stopwatch = Stopwatch.StartNew();
++this.frameCounter;
this.drawingImage.Drawing = this.Render();
}
string GenerateRandomString(int length)
{
var chars = new char[length];
for (int i = 0; i < chars.Length; ++i)
chars[i] = (char)rand.Next('A', 'Z' + 1);
return new string(chars);
}
void timer_Tick(object sender, EventArgs e)
{
var seconds = this.stopwatch.Elapsed.TotalSeconds;
Trace.WriteLine((long)(this.frameCounter / seconds));
if (seconds > 10)
{
this.stopwatch.Restart();
this.frameCounter = 0;
}
}
private Drawing Render()
{
var lines = new string[30];
for (int i = 0; i < lines.Length; ++i)
lines[i] = GenerateRandomString(100);
var drawing = new DrawingGroup();
using (var drawingContext = drawing.Open())
{
// TODO: draw rectangles which represent background.
// TODO: group of glyphs which has the same color should be drawn together.
// Following code draws all glyphs in Red color.
var glyphRun = ConvertTextLinesToGlyphRun(this.glyphTypeface, this.renderingEmSize, this.advanceWidth, this.advanceHeight, this.baselineOrigin, lines);
drawingContext.DrawGlyphRun(Brushes.Red, glyphRun);
}
return drawing;
}
static GlyphRun ConvertTextLinesToGlyphRun(GlyphTypeface glyphTypeface, double renderingEmSize, double advanceWidth, double advanceHeight, Point baselineOrigin, string[] lines)
{
var glyphIndices = new List<ushort>();
var advanceWidths = new List<double>();
var glyphOffsets = new List<Point>();
var y = baselineOrigin.Y;
for (int i = 0; i < lines.Length; ++i)
{
var line = lines[i];
var x = baselineOrigin.X;
for (int j = 0; j < line.Length; ++j)
{
var glyphIndex = glyphTypeface.CharacterToGlyphMap[line[j]];
glyphIndices.Add(glyphIndex);
advanceWidths.Add(0);
glyphOffsets.Add(new Point(x, y));
x += advanceWidth;
}
y += advanceHeight;
}
return new GlyphRun(
glyphTypeface,
0,
false,
renderingEmSize,
glyphIndices,
baselineOrigin,
advanceWidths,
glyphOffsets,
null,
null,
null,
null,
null);
}
}
}
This answer seems a bit flawed.
Possible cases:
The advance widths are different for each character
There is no effort to align the baselines, since a line may consist
of several glyph runs
Since the absolute y position is buried in the run it can't be easily moved when a line is inserted