.Net drawing clipping bug - winforms

GDI+ DrawLines function has a clipping bug that can be reproduced by running the following c# code. When running the code, two line paths appear, that should be identical, because both of them are inside the clipping region. But when the clipping region is set, one of the line segment is not drawn.
protected override void OnPaint(PaintEventArgs e)
{
PointF[] points = new PointF[] { new PointF(73.36f, 196),
new PointF(75.44f, 32),
new PointF(77.52f, 32),
new PointF(79.6f, 196),
new PointF(85.84f, 196) };
Rectangle b = new Rectangle(70, 32, 20, 164);
e.Graphics.SetClip(b);
e.Graphics.DrawLines(Pens.Red, points); // clipped incorrectly
e.Graphics.TranslateTransform(80, 0);
e.Graphics.ResetClip();
e.Graphics.DrawLines(Pens.Red, points);
}
Setting the antials mode on the graphics object resolves this. But that is not a real solution.
Does anybody know of a workaround?

It appears that this is a known bug...
The following code appears to function as you requested:
protected override void OnPaint(PaintEventArgs e)
{
PointF[] points = new PointF[] { new PointF(73.36f, 196),
new PointF(75.44f, 32),
new PointF(77.52f, 32),
new PointF(79.6f, 196),
new PointF(85.84f, 196) };
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
Rectangle b = new Rectangle(70, 32, 20, 165);
e.Graphics.SetClip(b);
e.Graphics.DrawLines(Pens.Red, points); // clipped incorrectly
e.Graphics.TranslateTransform(80, 0);
e.Graphics.ResetClip();
e.Graphics.DrawLines(Pens.Red, points);
}
Note: I have AntiAlias'ed the line and extended your clipping region by 1
it appears that the following work arounds might help (although not tested):
The pen is more than one pixel thick
The line is perfectly horizontal or vertical
The clipping is against the window boundaries rather than a clip rectangle
The following is a list of articles that might / or then again might not help:
http://www.tech-archive.net/pdf/Archive/Development/microsoft.public.win32.programmer.gdi/2004-08/0350.pdf
http://www.tech-archive.net/Archive/Development/microsoft.public.win32.programmer.gdi/2004-08/0368.html
OR...
the following is also possible:
protected override void OnPaint ( PaintEventArgs e )
{
PointF[] points = new PointF[] { new PointF(73.36f, 196),
new PointF(75.44f, 32),
new PointF(77.52f, 32),
new PointF(79.6f, 196),
new PointF(85.84f, 196) };
Rectangle b = new Rectangle( 70, 32, 20, 164 );
Region reg = new Region( b );
e.Graphics.SetClip( reg, System.Drawing.Drawing2D.CombineMode.Union);
e.Graphics.DrawLines( Pens.Red, points ); // clipped incorrectly
e.Graphics.TranslateTransform( 80, 0 );
e.Graphics.ResetClip();
e.Graphics.DrawLines( Pens.Red, points );
}
This effecivly clips using a region combined/unioned (I think) with the ClientRectangle of the canvas/Control. As the region is difned from the rectangle, the results should be what is expected. This code can be proven to work by adding
e.Graphics.FillRectangle( new SolidBrush( Color.Black ), b );
after the setClip() call. This clearly shows the black rectangle only appearing in the clipped region.
This could be a valid workaround if Anti-Aliasing the line is not an option.
Hope this helps

What appears to be the matter with the code?
OK, the question should be... what should the code do that it doesn't already.
When I run the code, I see 2 red 'spikes' am I not ment to?
You appear to draw the first spike within the clipped rectangle region verified by adding the the following after the declaration of teh Rectangle :
e.Graphics.FillRectangle( new SolidBrush( Color.Black ), b );
Then you perform a translation, reset the clip so at this point I assume the clientRectangle is being used as the appropriate clip region and then attempt to redarw the translated spike. Where's the bug?!?

The bug is that both line segments should be drawn identical but they are not because the spike that is drawn within the clipping region is completely within the clipping region and should not be clipped in any way but it is. This is a very annoying but that results in any software that uses drawlines heavily + clipping to look unprofessional because of gaps that can appear in the polygons.

Related

Why am I missing controls with bit

Simple problem.
I have a form to which I add a panel and put 1 label with some text. When I look at the saved image, all I see is the panel. I have tried all the solutions I could find. Code below. I get the panel saved but the text box doesn't appear. If I can get that to work, then I can do all that I need.
What am I doing wrong?
int x = SystemInformation.WorkingArea.X;
int y = SystemInformation.WorkingArea.Y;
int width = printPanel.Width;
int height = printPanel.Height;
Rectangle bounds = new Rectangle(x, y, width, height);
using (Bitmap flag = new Bitmap(width, height))
{
printPanel.DrawToBitmap(flag, bounds);
if (Environment.UserName == "grimesr")
{
string saveImage = Path.Combine(fileStore, "./p" + ".png");
flag.Save(saveImage);
}
}
Really not sure where you're going wrong.
Here's a simple test:
private void button1_Click(object sender, EventArgs e)
{
int width = printPanel.Width;
int height = printPanel.Height;
Rectangle bounds = new Rectangle(0, 0, width, height);
Bitmap flag = new Bitmap(width, height);
printPanel.DrawToBitmap(flag, bounds);
pictureBox1.Image = flag;
}
It grabs the entire panel and puts the image into the picturebox to the right:
Thanks for the hints, even if they weren't directly related. The print button helped me figure this out. The button code worked as desired. However, putting the same code where I had it wasn't working. I then noticed a InvalidOperation error was being rasied. So looking at more in detail led me to see what the real issue was.
I must admit I left out 1 tiny piece of information that was critical. I was trying to do this in a thread that was feeding my label printer. Of course, trying to used a UI panel control in the thread threw an Invalid Operation. I moved the code out and all is well now. Cross thread operations are sometimes subtle and difficult to think about how they fail.
Problem solved for me.

Generalizing the ScaleTransform (WPF) when image is not aligned to x-y axes

My linear algebra is weak. WPF is a great system for Rendering different transformations upon an image. However, the standard ScaleTransform will only scale the image's along the x-y axes. When the edges have first been rotated, the result of applying the ScaleTransform will result in a skewed transformation (as shown below) since the edges are no longer aligned.
So, if I have an image that has undergone several different transforms with the result being shown by the WPF rendering system, how do I calculate the correct matrix transform to take the (final rotated image) and scale it along the axes of the rendered image?
Any help or suggestions will be most appreciated.
TIA
(For the complete code, please see my previous question.)
Edit #1: To see the above effect:
Drop image onto Inkcavas. -- no skewing seen.
Rotate image counterclockwise (to about 45deg) -- no skewing seen.
Make the image larger (about twice its prescaled size -- no skewing seen.
Rotate the image clockwise (about back to where it started) -- skewing is
immediately seen during and after the rotation.
If step 3 is skipped, simple rotation -- no matter how many times done -- will not cause the skewing effect. Actually, this makes sense. The ScaleTransform preserves the distance from center from the edges of the image. If the image is at an angle, the x-y distance from the edges of the transform are no longer constant through the width and length of the rendered image. So the edges get appropriately scaled, but the angles are changed.
Here is the most relevant code:
private ImageResizing(Image image)
{
if (image == null)
throw new ArgumentNullException("image");
_image = image;
TransformGroup tg = new TransformGroup();
image.RenderTransformOrigin = new Point(0.5, 0.5); // All transforms will be based on the center of the rendered element.
tg.Children.Add(image.RenderTransform); // Keeps whatever transforms have already been applied.
image.RenderTransform = tg;
_adorner = new MyImageAdorner(image); // Create the adorner.
InstallAdorner(); // Get the Adorner Layer and add the Adorner.
}
Note: The image.RenderTransformOrigin = new Point(0.5, 0.5) is set to the center
of the rendered image. All transforms will be based on the center of the image at the time it is seem by the transform.
public MyImageAdorner(UIElement adornedElement)
: base(adornedElement)
{
visualChildren = new VisualCollection(this);
// Initialize the Movement and Rotation thumbs.
BuildAdornerRotate(ref moveHandle, Cursors.SizeAll);
BuildAdornerRotate(ref rotateHandle, Cursors.Hand);
// Add handlers for move and rotate.
moveHandle.DragDelta += new DragDeltaEventHandler(moveHandle_DragDelta);
moveHandle.DragCompleted += new DragCompletedEventHandler(moveHandle_DragCompleted);
rotateHandle.DragDelta += new DragDeltaEventHandler(rotateHandle_DragDelta);
rotateHandle.DragCompleted += new DragCompletedEventHandler(rotateHandle_DragCompleted);
// Initialize the Resizing (i.e., corner) thumbs with specialized cursors.
BuildAdornerCorner(ref topLeft, Cursors.SizeNWSE);
// Add handlers for resizing.
topLeft.DragDelta += new DragDeltaEventHandler(TopLeft_DragDelta);
topLeft.DragCompleted += TopLeft_DragCompleted;
// Put the outline border arround the image. The outline will be moved by the DragDelta's
BorderTheImage();
}
#region [Rotate]
/// <summary>
/// Rotate the Adorner Outline about its center point. The Outline rotation will be applied to the image
/// in the DragCompleted event.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void rotateHandle_DragDelta(object sender, DragDeltaEventArgs e)
{
// Get the position of the mouse relative to the Thumb. (All cooridnates in Render Space)
Point pos = Mouse.GetPosition(this);
// Render origin is set at center of the adorned element. (all coordinates are in rendering space).
double CenterX = AdornedElement.RenderSize.Width / 2;
double CenterY = AdornedElement.RenderSize.Height / 2;
double deltaX = pos.X - CenterX;
double deltaY = pos.Y - CenterY;
double angle;
if (deltaY.Equals(0))
{
if (!deltaX.Equals(0))
angle = 90;
else
return;
}
else
{
double tan = deltaX / deltaY;
angle = Math.Atan(tan); angle = angle * 180 / Math.PI;
}
// If the mouse crosses the vertical center,
// find the complementary angle.
if (deltaY > 0)
angle = 180 - Math.Abs(angle);
// Rotate left if the mouse moves left and right
// if the mouse moves right.
if (deltaX < 0)
angle = -Math.Abs(angle);
else
angle = Math.Abs(angle);
if (double.IsNaN(angle))
return;
// Apply the rotation to the outline. All Transforms are set to Render Center.
rotation.Angle = angle;
rotation.CenterX = CenterX;
rotation.CenterY = CenterY;
outline.RenderTransform = rotation;
}
/// Rotates image to the same angle as outline arround the render origin.
void rotateHandle_DragCompleted(object sender, DragCompletedEventArgs e)
{
// Get Rotation Angle from outline. All element rendering is set to rendering center.
RotateTransform _rt = outline.RenderTransform as RotateTransform;
// Add RotateTransform to the adorned element.
TransformGroup gT = AdornedElement.RenderTransform as TransformGroup;
RotateTransform rT = new RotateTransform(_rt.Angle);
gT.Children.Insert(0, rT);
AdornedElement.RenderTransform = gT;
outline.RenderTransform = Transform.Identity; // clear transform from outline.
}
#endregion //Rotate
#region [TopLeft Corner
// Top Left Corner is being dragged. Anchor is Bottom Right.
void TopLeft_DragDelta(object sender, DragDeltaEventArgs e)
{
ScaleTransform sT = new ScaleTransform(1 - e.HorizontalChange / outline.ActualWidth, 1 - e.VerticalChange / outline.ActualHeight,
outline.ActualWidth, outline.ActualHeight);
outline.RenderTransform = sT; // This will immediately show the new outline without changing the Image.
}
/// The resizing outline for the TopLeft is based on the bottom right-corner. The resizing transform for the
/// element, however, is based on the render origin being in the center. Therefore, the Scale transform
/// received from the outling must be recalculated to have the same effect--only from the rendering center.
///
/// TopLeft_DragCompleted resize the element rendering.
private void TopLeft_DragCompleted(object sender, DragCompletedEventArgs e)
{
// Get new scaling from the Outline.
ScaleTransform _sT = outline.RenderTransform as ScaleTransform;
scale.ScaleX *= _sT.ScaleX; scale.ScaleY *= _sT.ScaleY;
Point Center = new Point(AdornedElement.RenderSize.Width/2, AdornedElement.RenderSize.Height/2);
TransformGroup gT = AdornedElement.RenderTransform as TransformGroup;
ScaleTransform sT = new ScaleTransform( _sT.ScaleX, _sT.ScaleY, Center.X, Center.Y);
gT.Children.Insert(0, sT);
AdornedElement.RenderTransform = gT;
outline.RenderTransform = Transform.Identity; // Clear outline transforms. (Same as null).
}
#endregion
Note: I am adding each new transform to the first of the children list. This makes calculations on the image easier.
I could not find with Google or in text all the elements needed to answer this question completely. So, for all other newbies like my self, I will post this (very long) answer. (Editors and Gurus please feel free to correct).
A word toward setup. I have an inkcanvas onto which an image is dropped and added as a child of the inkcanvas. At the time of the drop, an adorner containing a Thumb on each corner for resizing, a Top-Middle thumb for rotating, and a middle thumb for translation is added for final positioning of the image. Along with a "outline" designed as a path element, the Thumbs and outline complete the Adorner and create a kind of wire frame around the adorned element.
There are multiple key points:
WPF first uses a layout pass to position elements within their parent container, followed by a rendering pass to arrange the element. Transforms can be applied to either or both the layout and rendering passes. However, it needs to be noted that the layout pass uses an x-y coordinate system with the origin on the top left of the parent, where as the rendering system inherently references the top left of the child element. If the layout position of the dropped element is not specifically defined, it will by default be added to the "origin" of the parent container.
The RenderTransform is by default a MatrixTransform but can be replaced by a TransformGroup. Using either or both of these allows for Matrices (in the MatrixTransform) or Transforms (in the TransformGroup) to be applied in any order. My preference was to use the MatrixTransforms to better see the relationship between scaling, rotation, and translation.
The rendering of the adorner follows the element it adorns. That is, the element's rendering will also be applied to the Adorner. This behavior can be overriden by use of
public override GeneralTransform GetDesiredTransform(GeneralTransform transform)
As noted in the initial question, I had avoided using SetTop() and SetLeft() as they messed up my other matrices. In hindsight, the reason my matrices failed was because SetTop() and SetLeft() apparently work during the layout phase--so all my coordinates for rendering were off. (I was using a TransalateTransform to position the image upon drag-drop.) However, using SetTop() and SetLeft() apparently act during the Layout Phase. Using this greatly simplified the calculations needed for the Rendering Phase since all coordinates could refer to the image without regard to the position on the canvas.
private void IC_Drop(object sender, DragEventArgs e)
{
InkCanvas ic = sender as InkCanvas;
// Setting InkCanvasEditingMode.None is necessary to capture DrawingLayer_MouseDown.
ic.EditingMode = InkCanvasEditingMode.None;
ImageInfo image_Info = e.Data.GetData(typeof(ImageInfo)) as ImageInfo;
if (image_Info != null)
{
// Display enlarged image on ImageLayer
// This is the expected format for the Uri:
// ImageLayer.Source = new BitmapImage(new Uri("/Images/Female - Front.png", UriKind.Relative));
// Source = new BitmapImage(image_Info.Uri);
Image image = new Image();
image.Width = image_Info.Width * 4;
// Stretch.Uniform keeps the Aspect Ratio but totally screws up resizing the image.
// Stretch.Fill allows for resizing the Image without keeping the Aspect Ratio.
image.Stretch = Stretch.Fill;
image.Source = new BitmapImage(image_Info.Uri);
// Position the drop. Note that SetLeft and SetTop are active during the Layout phase of the image drop and will
// be applied before the Image hits its Rendering stage.
Point position = e.GetPosition(ic);
InkCanvas.SetLeft(image, position.X);
InkCanvas.SetTop(image, position.Y);
ic.Children.Add(image);
ImageResizing imgResize = ImageResizing.Create(image);
}
}
Since I want to be able to resize the image from any direction, the image is set with Stretch.Fill. When Stretch.Uniform was used, the image appeared to first be resized then jump back to its initial size.
Since I am using MatrixTransform, the order of the Matrices is important. So when applying the Matrices, for my use
// Make new render transform. The Matrix order of multiplication is extremely important.
// Scaling should be done first, followed by (skewing), rotation and translation -- in
// that order.
MatrixTransform gT = new MatrixTransform
{
Matrix = sM * rM * tM
};
ele.RenderTransform = gT;
Scaling (sM), is performed before Rotation (rM). Translation is applied last. (C# does matrix multiplication from left to right).
In review of the matrices, it is apparent that the Rotation Matrix also involves skewing elements. (Which makes sense since apparently the RotationTransform is intended to keep the angles at the edges constant). Thus, the Rotation Matrix depends on the size of the image.
In my case, the reason scaling after rotation was causing skewing is because the Scaling transform multiplies the distance between points of the image and the x-y axes. So if the edge of the image is not of constant distance to the x-y axes, scaling will distort (i.e., skew) the image.
Putting this together, results in the following method to resize the image:
Action<Matrix, Vector> DragCompleted = (growthMatrix, v) =>
{
var ele = AdornedElement;
// Get the change vector. Transform (i.e, Rotate) change vector into x-y axes.
// The Horizontal and Vertical changes give the distance between the the current cursor position
// and the Thumb.
Matrix m = new Matrix();
m.Rotate(-AngleDeg);
Vector v1 = v * m;
// Calculate Growth Vector.
var gv = v1 * growthMatrix;
// Apply new scaling along the x-y axes to obtain the rendered size.
// Use the current Image size as the reference to calculate the new scaling factors.
var scaleX = sM.M11; var scaleY = sM.M22;
var W = ele.RenderSize.Width * scaleX; var H = ele.RenderSize.Height * scaleY;
var sx = 1 + gv.X/ W; var sy = 1 + gv.Y / H;
// Change ScalingTransform by applying the new scaling factors to the existing scaling transform.
// Do not add offsets to the scaling transform matrix as they will be included in future scalings.
// With RenderTransformOrigin set to the image center (0.5, 0.5), scalling occurs from the center out.
// Move the new center of the new resized image to its correct position such that the image's thumb stays
// underneath the cursor.
sM.Scale(sx, sy);
tM.Translate(v.X / 2, v.Y / 2);
// New render transform. The order of the transform's is extremely important.
MatrixTransform gT = new MatrixTransform
{
Matrix = sM * rM * tM
};
ele.RenderTransform = gT;
outline.RenderTransform = Transform.Identity; // clear this transform from the outline.
};
Just to be clear, my "Growth Matrix" is defined in such a manner as to result in "Positive" growth as the cursor is moved away from the center of the image. For Example, the TopLeft corner will "grow" the image when moved to the left and up. Hence
growth matrix = new Matrix(-1, 0, 0, -1, 0, 0) for top-left corner.
The last problem is that of correctly calculating the rotation center (i.e., I want to spin, not orbit). This becomes greatly simplified by using
// All transforms will be based on the center of the rendered element.
AdornedElement.RenderTransformOrigin = new Point(0.5, 0.5);
Lastly, since I am scaling from a corner, the center of the image needs to be translated to keep the corner underneath the cursor.
Sorry for the length of this answer, but there is much to cover (and learn :) ). Hope this helps somebody.

GMap.Net location on marker centralizes over point not above

I have just started using GMAP.Net and I'm Setting a custom Marker thus:
marker = new GMarkerGoogle(new PointLatLng(Convert.ToDouble(latlon[0]), Convert.ToDouble(latlon[1])), new Bitmap(Iconpath));
where Iconpath points to a 42 * 38 pixel image of type PNG.
however the image appears central and immediately above the point being set by the above. What I would like is to know how to set it so the center of the image in over the location.
Any idea how to do that?
this is in a winforms .Net 4.0 application.
I found there was an easy way to do this thus:
Bitmap imgMarker = new Bitmap(Iconpath);
marker = new GMarkerGoogle(new PointLatLng(Convert.ToDouble(latlon[0]), Convert.ToDouble(latlon[1])), imgMarker);
marker.Offset = new Point(imgMarker.Width/2, imgMarker.Height / 2);
hope it helps someone else!
The GoogleMarker class seems to be designed for push pin looking images, where you would want the pin tip to be directly above the point of interest. Your best bet is to inherit the marker class and make your own class where you can control the image placement. Like this:
class customImageMarker: GMapMarker
{
Bitmap Bitmap;
public customImageMarker(PointLatLng p, Bitmap Bitmap)
: base(p)
{
this.Bitmap = Bitmap;
Size = new System.Drawing.Size(Bitmap.Width, Bitmap.Height);
Offset = new Point(-Size.Width / 2, -Size.Height / 2);
}
public override void OnRender(Graphics g)
{
g.DrawImage(Bitmap, LocalPosition.X - Offset.X, LocalPosition.Y - Offset.Y, Size.Width, Size.Height);
}
}
Now just call your class:
marker = new customImageMarker(new PointLatLng(Convert.ToDouble(latlon[0]), Convert.ToDouble(latlon[0])), new Bitmap(Iconpath));
You can control the placement of the icon by adjusting the Offset variable.

Drawing a Rectangle at clicked point on canvas wpf

Losing my mind here. I spent the last couple days writing the beginnings of a windows app in WinForms... yesterday did a 90 degree turn and had to switch over to WPF for aesthetic reasons. Been having some trouble converting certain things over... namely anything graphics related. In the wpf version, I had the following C# code to created a black circle and white rectangle at the point where the user clicked on the panel and display a message box displaying what the x coordinate is:
Graphics g;
private void panel1_MouseClick(object sender, MouseEventArgs e)
{
SolidBrush s2 = new SolidBrush(Color.Black);
g.FillRectangle(s2, e.X + 3, e.Y + 3, 10, 10);
SolidBrush s = new SolidBrush(Color.White);
g.FillPie(s, e.X + 4, e.Y + 4, 7, 7, 0, 360);
float x_cord = e.X;
MessageBox.Show("X is: " + x_cord.ToString());
}
I tried to do this in wfb via the following code:
Graphics g;
private void Canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
System.Windows.Point position = e.GetPosition(this);
double pX = position.X;
double pXint = Convert.ToInt32(pX);
MessageBox.Show(pX.ToString());
SolidBrush s2 = new SolidBrush(System.Drawing.Color.Black);
g.FillRectangle(s2, pXint, 5, 10, 10);
}
This compiles and runs correctly but when I click on canvas, nothing happens! The messagebox was there mainly as a test to see if the event was triggering at all, which it clearly isn't. Here's the corresponding XML for the window with the canvas in it:
<Grid>
<Canvas Name="Surface" Height="Auto" Width="Auto" Background="White"> </Canvas>
</Grid>
What am I doing wrong here? I actually like WPF better for the most part but certain things are an absolute nuisance.
The problem is you are trying to Draw System.Drawing objects onto a WPF Canvas, you cant do that.
One way to solve this is to just add Rectangles to you Canvas.
<Grid>
<Canvas Name="Surface" Height="Auto" Width="Auto" Background="White" MouseLeftButtonDown="Surface_MouseLeftButtonDown"></Canvas>
</Grid>
private void Surface_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Rectangle rect = new Rectangle { Width = 10, Height = 10, Fill = Brushes.Black };
Surface.Children.Add(rect);
Canvas.SetLeft(rect, e.GetPosition(this).X);
Canvas.SetTop(rect, e.GetPosition(this).Y);
}
Got it. Can't believe it took me 6 hours to draw a Rectangle. Not exactly sure what made it work but think it had to do somewhat with getting rid of using System.Drawing; When I tried sa_ddam213's code I got an error saying "Cannot convert from System.Drawing.Rectangle to System.Windows.UIElement" for unknown reasons. After google search number 5007 I came upon this article:
http://msdn.microsoft.com/en-us/library/ms747393.aspx
I had looked at it before but couldn't get the code to compile for the line or the elipse, I'm thinking it was System.Drawing assembly causing problems.
Thanks so much for the help all!!

WPF DrawingContext seems ignore SnapToDevicePixels

I'm drawing a chart by direct calls to DrawLine on the DrawingContext. Since I want to avoid any anti aliasing feature, I tryed to put the SnapToDevicePixels=true on the parent UIElement, but I still have anti-alias:
The project was an old OS project not written for WPF4, but I retarget it to the Framework4, can this be an issue too ?
I found this link where they basically say you should set
ParentUIElement.SetValue(RenderOptions.EdgeModeProperty, EdgeMode.Aliased);
It worked for me, so it might be worth a try!
SnapsToDevicePixels works only for element bounding box. You need to use Guidelines with DrawingContext. Also you can specify VisualXSnappingGuidelines and VisualYSnappingGuidelines if it fits to your requirements.
GuidelineSet is designed to resolve your problems.
#Marat Khasanov recommended you to use GuidelineSet and you replied that it messed up your code. I'm also suffering from this problem, so I write the code below to solve this problem with non-ugly code.
Notice: This method works even in a Viewbox.
public static class SnapDrawingExtensions
{
public static void DrawSnappedLinesBetweenPoints(this DrawingContext dc,
Pen pen, double lineThickness, params Point[] points)
{
var guidelineSet = new GuidelineSet();
foreach (var point in points)
{
guidelineSet.GuidelinesX.Add(point.X);
guidelineSet.GuidelinesY.Add(point.Y);
}
var half = lineThickness / 2;
points = points.Select(p => new Point(p.X + half, p.Y + half)).ToArray();
dc.PushGuidelineSet(guidelineSet);
for (var i = 0; i < points.Length - 1; i = i + 2)
{
dc.DrawLine(pen, points[i], points[i + 1]);
}
dc.Pop();
}
}
Call the method in OnRender and pass line points to it.
protected override void OnRender(DrawingContext dc)
{
// Draw four horizontal lines and one vertical line.
// Notice that even the point X or Y is not an integer, the line is still snapped to device.
dc.DrawSnappedLinesBetweenPoints(_pen, LineThickness,
new Point(0, 0), new Point(320, 0),
new Point(0, 40), new Point(320, 40),
new Point(0, 80.5), new Point(320, 80.5),
new Point(0, 119.7777), new Point(320, 119.7777),
new Point(0, 0), new Point(0, 120));
}
Here is the render result in a viewbox:

Resources