I'm using DrawingContext class to draw a bar graph, but the produced labels and ticks are blurry:
In the code I am using a class BarRenderer derived from FrameworkElement. Each bar is represented by a BarRenderer instance, and I do the drawing by overriding the OnRender method. Each "bar" has a label and a value where label is the text below and value defines the height of the bar. and The logic responsible for drawing labels and tick is as follows:
private void DrawLabel(DrawingContext drawingContext, FormattedText text, double x, double y, int direction)
{
drawingContext.DrawLine(_blackPen, new Point(Width * 0.5, y), new Point(Width * 0.5, y + 6));
y += 6;
if (LabelRotationDegree != 0)
{
RotateTransform rotateTransform = new RotateTransform(-LabelRotationDegree, 0.5 * Width, y + text.Height * 0.5);
TranslateTransform translateTransform = new TranslateTransform(0, direction * text.WidthIncludingTrailingWhitespace * 0.5);
drawingContext.PushTransform(translateTransform);
drawingContext.PushTransform(rotateTransform);
}
drawingContext.DrawText(text, new Point(x, y));
if (LabelRotationDegree != 0)
{
drawingContext.Pop();
drawingContext.Pop();
}
}
I doubt that the issue is with the rotation transform, since when the labels are not transformed they are no longer as blurry as before:
But as can be seen some of the ticks are darker than the others. So what am I doing wrong?
EDIT:
I've already set SnapsToDevicePixels to true, and when I set RenderOptions.SetEdgeMode to Aliased some ticks disappear and text is still blurry:
Edit 2: More code
public class GraphUnitRenderer : FrameworkElement
{
private const double TEXT_SIZE = 12;
private const double KEY_LABELS_HEIGHT = 50;
private readonly Typeface _typeface;
private readonly CultureInfo _cultureInfo;
private readonly Pen _blackPen;
private readonly Pen _bluePen;
private readonly Pen _tickBlackPen;
private readonly Pen _whitePen;
public BarData Data { get; set; }
//Some properties
public GraphUnitRenderer()
{
_typeface = new Typeface("Segoe UI");
_cultureInfo = CultureInfo.CurrentCulture;
_blackPen = new Pen(Brushes.Black, 1);
_bluePen = new Pen(Brushes.Blue, 1);
_tickBlackPen = new Pen(Brushes.Black, 2);
_whitePen = new Pen(Brushes.White, 1);
/*
_blackPen.Freeze();
_bluePen.Freeze();
_tickBlackPen.Freeze();
_whitePen.Freeze();
*/
RenderOptions.SetEdgeMode(this, EdgeMode.Aliased);
SnapsToDevicePixels = true;
}
protected override void OnRender(DrawingContext drawingContext)
{
TextOptions.SetTextFormattingMode(this, TextFormattingMode.Display);
FormattedText keyText = GetText(Data.Key);
//Bar drawing logic
if (LabelDisplayIsForced || (LabelRotationDegree == 0 && keyText.WidthIncludingTrailingWhitespace < Width) || (LabelRotationDegree != 0 && keyText.Height < Width))
DrawLabel(drawingContext, keyText, 0.5 * (Width - keyText.WidthIncludingTrailingWhitespace), GetYPosition(1) + 1, 1);
}
private void DrawLabel(DrawingContext drawingContext, FormattedText text, double x, double y, int direction)
{
drawingContext.DrawLine(_blackPen, new Point(Width * 0.5, y), new Point(Width * 0.5, y + 6));
y += 6;
if (LabelRotationDegree != 0)
{
RotateTransform rotateTransform = new RotateTransform(-LabelRotationDegree, 0.5 * Width, y + text.Height * 0.5);
TranslateTransform translateTransform = new TranslateTransform(0, direction * text.WidthIncludingTrailingWhitespace * 0.5);
drawingContext.PushTransform(translateTransform);
drawingContext.PushTransform(rotateTransform);
}
drawingContext.DrawText(text, new Point(x, y));
if (LabelRotationDegree != 0)
{
drawingContext.Pop();
drawingContext.Pop();
}
}
private double GetYPosition(double position) => Height - position - KEY_LABELS_HEIGHT;
private FormattedText GetText(string text) => new FormattedText(text, _cultureInfo, FlowDirection.LeftToRight, _typeface, TEXT_SIZE, Brushes.Black, VisualTreeHelper.GetDpi(this).PixelsPerDip);
}
Related
I have the following code in my WPF application:
public MainWindow()
{
InitializeComponent();
p1 = new Point(50, 50);
p2 = new Point(355, 50);
p3 = new Point(50, 355);
p4 = new Point(355, 355);
Loaded += (x, y) => Draw();
//ContentRendered += (x, y) => Draw();
}
I create a Polygon shape and try to get its bounding rectangle several ways:
private void Draw()
{
Polygon polygon = new Polygon();
polygon.Stroke = System.Windows.Media.Brushes.White;
polygon.Points.Add(p1);
polygon.Points.Add(p2);
polygon.Points.Add(p3);
polygon.Points.Add(p4);
canvas.Children.Add(polygon);
boundingRect = polygon.TransformToVisual(canvas).TransformBounds(new Rect(polygon.RenderSize));
boundingRect = polygon.TransformToVisual(polygon).TransformBounds(System.Windows.Controls.Primitives.LayoutInformation.GetLayoutSlot(polygon));
boundingRect = polygon.RenderTransform.TransformBounds(new Rect(polygon.RenderSize));
boundingRect = GetRectOfObject(polygon);
boundingRect = polygon.RenderedGeometry.Bounds;
}
private Rect GetRectOfObject(FrameworkElement _element)
{
Rect rectangleBounds = new Rect();
rectangleBounds = _element.RenderTransform.TransformBounds(new Rect(0, 0, _element.ActualWidth, _element.ActualHeight));
return rectangleBounds;
}
However, I always get {0;0;0;0}.
On debug, I can see the points but its size is rendered 0:
How should I calculate the correct way?
According to the MSDN remarks of the Measure method, calling UpdateLayout will obtain the necessary data (though it should be treated carefully), so this way the measuring works:
private void Draw()
{
Polygon polygon = new Polygon();
polygon.Stroke = System.Windows.Media.Brushes.White;
polygon.Points.Add(p1);
polygon.Points.Add(p2);
polygon.Points.Add(p3);
polygon.Points.Add(p4);
canvas.Children.Add(polygon);
polygon.UpdateLayout();
boundingRect = polygon.TransformToVisual(canvas).TransformBounds(new Rect(polygon.RenderSize));
}
Thanks #ChrisF for the hints.
I was trying to make a method to draw some arrowheads from a PNG image with transparency.
While I moving mouse, the application will be plot that png along the path.
What is the way to make that?
Another form I was wondering is creating a polygnon shape. I was tryed draw the shape (triangle) when enter on mouse down event.
void MainWindow_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
double posX = e.GetPosition(null).X;
double posY = e.GetPosition(null).Y;
double posX1 = posX - (posX / 4);
double posY1 = posY - (posY / 4);
double posX2 = posX + (posX / 4);
double posY2 = posY;
double posX3 = posX - (posX / 4);
double posY3 = posY + (posY - posY / 3);
Polygon p = new Polygon();
p.Stroke = Brushes.Black;
p.Fill = Brushes.LightBlue;
p.StrokeThickness = 1;
p.HorizontalAlignment = HorizontalAlignment.Left;
p.VerticalAlignment = VerticalAlignment.Center;
p.Points = new PointCollection() { new Point(posX1, posY1), new Point(posX2, posY2), new Point(posX3, posY3), new Point(posX1, posY1) };
MyCanvas.Children.Add(p);
}
But still can't click and draw a triangle correctly in mouse (x,y) position.
http://i.stack.imgur.com/bM2i4.png
Here is a quick example.
XAML
<Canvas x:Name='DrawingCanvas'
Margin='30'
Background='LightBlue'
MouseDown='Canvas_MouseDown'
MouseMove='Canvas_MouseMove'
MouseUp='Canvas_MouseUp'>
<Rectangle>
<Rectangle.Fill>
<ImageBrush ImageSource='Arrow.png' x:Name='ArrowBrush'/>
</Rectangle.Fill>
</Rectangle>
</Canvas>
CODE
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
}
private bool _isDrawing = false;
private Point _lastPosition;
const double MIN_MOVEMENT = 60;
private void Canvas_MouseDown(object sender, MouseButtonEventArgs e) {
_isDrawing = true;
_lastPosition = e.GetPosition(DrawingCanvas);
// add the first arrow
AddArrow(_lastPosition);
}
private void Canvas_MouseMove(object sender, MouseEventArgs e) {
if (!_isDrawing)
{
return;
}
var currentPosition = e.GetPosition(DrawingCanvas);
// draw a new image if mouse has traveled minimum distance
if (Math.Abs((currentPosition.X - _lastPosition.X)) > MIN_MOVEMENT ||
Math.Abs((currentPosition.Y - _lastPosition.Y)) > MIN_MOVEMENT)
{
AddArrow(currentPosition);
_lastPosition = e.GetPosition(DrawingCanvas);
}
}
private void AddArrow(Point currentPosition) {
var rect = new Rectangle();
rect.Width = 120;
rect.Height = 120;
rect.Fill = ArrowBrush;
Canvas.SetTop(rect, currentPosition.Y);
Canvas.SetLeft(rect, currentPosition.X);
DrawingCanvas.Children.Add(rect);
}
private void Canvas_MouseUp(object sender, MouseButtonEventArgs e) {
_isDrawing = true;
}
}
Screenshot
I have used the PAINT event to draw a wave on a Panel in Winows Form Application. But when using it WPF, I didn't find any such element equivalent to a Panel which has a Paint Event. Googled a lot too but no great use.
Well, I need to draw a waveform in WPF so suggest appropriate solutions wrt PaintArgsEvent or a new solution altogether.
Thank You!
You are looking for the DrawingVisual Class
From first Link:
The DrawingVisual is a lightweight drawing class that is used to render shapes, images, or text. This class is considered lightweight because it does not provide layout or event handling, which improves its performance. For this reason, drawings are ideal for backgrounds and clip art.
You also have access to a PolyLine Class that you can add a point Collection to. This example is a modified MSDN Forum example
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
float x0 = 100f;
float y0 = 100f;
Polyline myPoly = new Polyline();
PointCollection polyPoints = myPoly.Points;
Point[] points = new Point[200];
for (int j = 0; j < 200; j++)
{
points[j] = new Point();
points[j].X = x0 + j;
points[j].Y = y0 -
(float)(Math.Sin((2 * Math.PI * j) / 200) * (200 / (2 * Math.PI)));
}
for (int i = 0; i < points.Length ; i++)
{
polyPoints.Add(points[i]);
}
myPoly.Stroke = Brushes.Green;
myPoly.StrokeThickness = 5;
StackPanel mainPanel = new StackPanel();
mainPanel.Children.Add(myPoly);
this.Content = mainPanel;
}
}
And a Modified MSDN example:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
float x0 = 100f;
float y0 = 100f;
Point[] points = new Point[200];
for (int j = 0; j < 200; j++)
{
points[j] = new Point();
points[j].X = x0 + j;
points[j].Y = y0 -
(float)(Math.Sin((2 * Math.PI * j) / 200) * (200 / (2 * Math.PI)));
}
DrawingBrush db = new DrawingBrush(CreateDrawingVisualRectangle(points).Drawing);
StackPanel mainPanel = new StackPanel();
mainPanel.Background = db;
this.Content = mainPanel;
}
private DrawingVisual CreateDrawingVisualRectangle( Point[] pointarray)
{
DrawingVisual drawingVisual = new DrawingVisual();
// Retrieve the DrawingContext in order to create new drawing content.
DrawingContext drawingContext = drawingVisual.RenderOpen();
// Create a rectangle and draw it in the DrawingContext.
for (int i = 0; i < pointarray.Length-1; i++)
{
drawingContext.DrawLine(new Pen(new SolidColorBrush(Colors.Blue), 2), pointarray[i], pointarray[i + 1]);
}
// Persist the drawing content.
drawingContext.Close();
return drawingVisual;
}
}
I want to display an 8x8 Grid in Windows 8 metro app. To do this:
I created a Grid, and added 8 row definitions and 8 column definitions.
I then add a Rectangle with a black border to each of the grid cells.
Then in the MeasureOverride method, I check the availableSize. Since my grid needs to be square (aspect ratio = 1.0), I compute the minimum of availableSize.Width, availableSize.Height and return a new Size equal to (minimum, minimum).
However this does not work. The resulting grid's size is equal to availableSize, and not the size I return from my MeasureOverride method. If I modify the MeaureOverride, so that I set Height of RowDefinitions to minimum, and Width of ColumnDefinitions to minimum, then it works. But I saw some videos and they say you should not be explicitly setting Height & Width properties of anything.
So, is there a better way to accomplish what I want?
I'm not sure if you need to interact with those cells in any way, but if you just want to draw a grid, here is a quick control to do it. It will fill the space of the parent control.
public class GridShape : Control
{
public int Columns
{
get { return (int)GetValue(ColumnsProperty); }
set { SetValue(ColumnsProperty, value); }
}
public static readonly DependencyProperty ColumnsProperty = DependencyProperty.Register("Columns", typeof(int), typeof(GridShape), new PropertyMetadata(8));
public int Rows
{
get { return (int)GetValue(RowsProperty); }
set { SetValue(RowsProperty, value); }
}
public static readonly DependencyProperty RowsProperty = DependencyProperty.Register("Rows", typeof(int), typeof(GridShape), new PropertyMetadata(8));
public Brush Stroke
{
get { return (Brush)GetValue(StrokeProperty); }
set { SetValue(StrokeProperty, value); }
}
public static readonly DependencyProperty StrokeProperty = DependencyProperty.Register("Stroke", typeof(Brush), typeof(GridShape), new PropertyMetadata(new SolidColorBrush(Colors.Black)));
public double StrokeThickness
{
get { return (double)GetValue(StrokeThicknessProperty); }
set { SetValue(StrokeThicknessProperty, value); }
}
public static readonly DependencyProperty StrokeThicknessProperty = DependencyProperty.Register("StrokeThickness", typeof(double), typeof(GridShape), new PropertyMetadata(1.0));
protected override void OnRender(System.Windows.Media.DrawingContext drawingContext)
{
Pen pen = new Pen(Stroke, StrokeThickness);
double heightSpan = ActualHeight / Rows;
double widthSpan = ActualWidth / Columns;
for (double y = 0; y <= ActualHeight; y += heightSpan)
drawingContext.DrawLine(pen, new Point(0, y), new Point(ActualWidth, y));
for (double x = 0; x <= ActualWidth; x += widthSpan)
drawingContext.DrawLine(pen, new Point(x, 0), new Point(x, ActualHeight));
}
}
One solution is to create a custom Grid control to handle the width, height
public class SquareGrid : Grid
{
public SquareGrid()
{
this.SizeChanged += OnSizeChanged;
this.Loaded += OnLoaded;
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
var parent = VisualTreeHelper.GetParent(this) as FrameworkElement;
if (parent == null) return;
ResizeToSquare(parent);
}
private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
var parent = VisualTreeHelper.GetParent(this) as FrameworkElement;
if (parent == null) return;
parent.SizeChanged += ParentOnSizeChanged;
}
private void ParentOnSizeChanged(object sender, SizeChangedEventArgs sizeChangedEventArgs)
{
FrameworkElement parent = sender as FrameworkElement;
if (parent == null) return;
ResizeToSquare(parent);
}
private void ResizeToSquare(FrameworkElement parent)
{
var min = Math.Min(parent.ActualHeight, parent.ActualWidth);
this.Width = min;
this.Height = min;
}
}
You could also build a Behavior for this that would do the same thing.
I have a custom WPF control MyLine that should represent or not some text in its middle.
public class MyLine : Shape
{
public double X1, Y1, X2, Y2;
public bool IsTextDisplayed;
public string Caption;
protected override System.Windows.Media.Geometry DefiningGeometry
{
get
{
var geometryGroup = new GeometryGroup();
if (IsTextDisplayed)
{
// calculate text point
var midPoint = new Point((X1 + X2) / 2.0, (Y1 + Y2) / 2.0);
// add 'Caption' text in that point
// ???
}
// Add line
geometryGroup.Children.Add(new LineGeometry(
new Point(X1, Y1), new Point(X2, Y2)));
return geometryGroup;
}
}
}
So, how should I add the text here?
Create a FormattedText object and then create a Geometry from it:
FormattedText ft = new FormattedText(
"Caption",
Thread.CurrentThread.CurrentCulture,
System.Windows.FlowDirection.LeftToRight,
new Typeface("Verdana"), 32, Brushes.Black);
Geometry geometry = ft.BuildGeometry(midpoint);
geometryGroup.Children.Add(geometry);