I'm out to write some attached properties as suggested in Pushing read-only GUI properties back into ViewModel
I've written the following unit test:
private const double Dimension = 10.0;
[Test]
[RequiresSTA]
public void Gets_ActualWidth()
{
var rectangle = new Rectangle() { Width = Dimension, Height = Dimension };
double actualWidthMeasurement = Measurements.GetActualWidth(rectangle);
Assert.That(actualWidthMeasurement, Is.EqualTo(Dimension));
}
This is too naive though, the rectangle has an ActualWidth of 0 because no layout has been calculated.
Is there a simple way I can get a Rectangle with it's layout calculated.
I tried adding it to a StackPanel and calling Arrange(new Rect(0,0,20,20)), but still got a rectangle with ActualWidth/ActualHeight = 0.0d.
SOLUTION
[Test]
[RequiresSTA]
public void Gets_ActualWidth()
{
var rectangle = new Rectangle() { Width = Dimension, Height = Dimension};
rectangle.Measure(new Size(20, 20));
rectangle.Arrange(new Rect(0, 0, 20, 20));
double actualWidthMeasurement = Measurements.GetActualWidth(rectangle);
Assert.That(actualWidthMeasurement, Is.EqualTo(Dimension));
}
I don't see that you called Measure. That should be called before Arrange or else the Arrange will fail as everything has a DesiredSize of 0,0.
myStackPanel.Measure(new Size(20, 20));
myStackPanel.Arrange(new Rect(0, 0, 20, 20));
Related
I have problem with GDI. I do it in WinForms. There is what I got:
And there is my code:
Graphics phantom = this.pictureBox1.CreateGraphics();
Pen blackPen = new Pen(Color.Black, 3);
Rectangle rect = new Rectangle(0, 0, 200, 150);
float startAngle = 180F;
float sweepAngle = 180F;
phantom.DrawArc(blackPen, rect, startAngle, sweepAngle);
phantom.Dispose();
I want to get something like that:
Really sorry for my paint skills. Is it possible to create such a thing from the arc itself or do I have to do it from an ellipse? I don't know how to go about it. Any tips are welcome. Thanks.
From my comments on the original post:
You have two circles, let's call them lower and upper. Define the
upper circle as a GraphicsPath and pass that to the constructor of a
Region. Now pass that Region to e.Graphics via the ExcludeClip method.
Now draw the lower circle, which will be missing the top part because
of the clipping. Next, Reset() the Graphics and define the lower
circle in a GraphicsPath. Use Graphics.Clip() this time, and chase
that with drawing the upper circle. It will only be visible where the
lower circle clip was.
Proof of concept:
Code:
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
Graphics phantom = e.Graphics;
using (Pen blackPen = new Pen(Color.Black, 3))
{
Rectangle upper = new Rectangle(-50, -250, 300, 300);
GraphicsPath upperGP = new GraphicsPath();
upperGP.AddEllipse(upper);
using (Region upperRgn = new Region(upperGP))
{
Rectangle lower = new Rectangle(0, 0, 200, 150);
GraphicsPath lowerGP = new GraphicsPath();
lowerGP.AddEllipse(lower);
float startAngle = 180F;
float sweepAngle = 180F;
phantom.ExcludeClip(upperRgn);
phantom.DrawArc(blackPen, lower, startAngle, sweepAngle);
phantom.ResetClip();
phantom.SetClip(lowerGP);
phantom.DrawEllipse(blackPen, upper);
}
}
}
I have customized winform-design using region property as follows,
Region = System.Drawing.Region.FromHrgn(CreateRoundRectRgn(0, 0, varPassedInConstructor * 9, Height, 10, 10));
And here calling winform through following code in a new thread
new Thread(new ThreadStart(() => {
toast toast = new toast(message);
toast.Show(nativeWindow);
toast.Refresh();
Thread.Sleep(3000);
while (toast.Opacity > 0)
{
toast.Opacity -= 0.04;
Thread.Sleep(100);
}
toast.Close();
toast.Dispose();
})).Start();
Everything goes well, form is displayed properly initially, but before closing all of sudden, changes applied via Region gets disappeared and form appears like the one that is at design time.
Image one, When initially form displayed,
Image two, just before form is getting closed,
I tried lots of diff thing, I am not getting what exactly problem is, so all help will be appreciated.
Finally, I got the fix, instead of using CreateRoundRectRgn from GDI32 used a GraphicsPath approach as follows,
private void SetRegion()
{
var GP = RoundedRect(this.ClientRectangle, 5);
this.Region = new Region(GP);
}
And here is code for RoundRect function (Credit goes to https://stackoverflow.com/a/33853557/3531672),
public static GraphicsPath RoundedRect(Rectangle bounds, int radius)
{
int diameter = radius * 2;
Size size = new Size(diameter, diameter);
Rectangle arc = new Rectangle(bounds.Location, size);
GraphicsPath path = new GraphicsPath();
if (radius == 0)
{
path.AddRectangle(bounds);
return path;
}
// top left arc
path.AddArc(arc, 180, 90);
// top right arc
arc.X = bounds.Right - diameter;
path.AddArc(arc, 270, 90);
// bottom right arc
arc.Y = bounds.Bottom - diameter;
path.AddArc(arc, 0, 90);
// bottom left arc
arc.X = bounds.Left;
path.AddArc(arc, 90, 90);
path.CloseFigure();
return path;
}
then in constructor simply had set size of form itself and called SetRegion defined above,
this.Width = toastMessage.Length * 9;
SetRegion();
Please note additionally, I would recommend to override OnSizeChanged and simply call SetRegion in it.
I'm generating a bunch of RectangleF objects having different sizes and positions. What would be the best way to fill them with a gradient Brush in GDI+?
In WPF I could create a LinearGradientBrush, set Start and End relative points and WPF would take care of the rest.
In GDI+ however, the gradient brush constructor requires the position in absolute coordinates, which means I have to create a Brush for each of the rectangle, which would be a very complex operation.
Am I missing something or that's indeed the only way?
You can specify a transform at the moment just before the gradient is applied if you would like to declare the brush only once. Note that using transformations will override many of the constructor arguments that can be specified on a LinearGradientBrush.
LinearGradientBrush.Transform Property (System.Drawing.Drawing2D)
To modify the transformation, call the methods on the brush object corresponding to the desired matrix operations. Note that matrix operations are not commutative, so order is important. For your purposes, you'll probably want to do them in this order for each rendition of your rectangles: Scale, Rotate, Offset/Translate.
LinearGradientBrush.ResetTransform Method # MSDN
LinearGradientBrush.ScaleTransform Method (Single, Single, MatrixOrder) # MSDN
LinearGradientBrush.RotateTransform Method (Single, MatrixOrder) # MSDN
LinearGradientBrush.TranslateTransform Method (Single, Single, MatrixOrder) # MSDN
Note that the system-level drawing tools don't actually contain a stock definition for gradient brush, so if you have performance concerns about making multiple brushes, creating a multitude of gradient brushes shouldn't cost any more than the overhead of GDI+/System.Drawing maintaining the data required to define the gradient and styling. You may be just as well off to create a Brush per rectangle as needed, without having to dive into the math required to customize the brush via transform.
Brush Functions (Windows) # MSDN
Here is a code example you can test in a WinForms app. This app paints tiles with a gradient brush using a 45 degree gradient, scaled to the largest dimension of the tile (naively calculated). If you fiddle with the values and transformations, you may find that it isn't worth using the technique setting a transform for all of your rectangles if you have non-trivial gradient definitions. Otherwise, remember that your transformations are applied at the world-level, and in the GDI world, the y-axis is upside down, whereas in the cartesian math world, it is ordered bottom-to-top. This also causes the angle to be applied clockwise, whereas in trigonometry, the angle progresses counter-clockwise in increasing value for a y-axis pointing up.
using System.Drawing.Drawing2D;
namespace TestMapTransform
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Paint(object sender, PaintEventArgs e)
{
Rectangle rBrush = new Rectangle(0,0,1,1);
Color startColor = Color.DarkRed;
Color endColor = Color.White;
LinearGradientBrush br = new LinearGradientBrush(rBrush, startColor, endColor, LinearGradientMode.Horizontal);
int wPartitions = 5;
int hPartitions = 5;
int w = this.ClientSize.Width;
w = w - (w % wPartitions) + wPartitions;
int h = this.ClientSize.Height;
h = h - (h % hPartitions) + hPartitions;
for (int hStep = 0; hStep < hPartitions; hStep++)
{
int hUnit = h / hPartitions;
for (int wStep = 0; wStep < wPartitions; wStep++)
{
int wUnit = w / wPartitions;
Rectangle rTile = new Rectangle(wUnit * wStep, hUnit * hStep, wUnit, hUnit);
if (e.ClipRectangle.IntersectsWith(rTile))
{
int maxUnit = wUnit > hUnit ? wUnit : hUnit;
br.ResetTransform();
br.ScaleTransform((float)maxUnit * (float)Math.Sqrt(2d), (float)maxUnit * (float)Math.Sqrt(2d), MatrixOrder.Append);
br.RotateTransform(45f, MatrixOrder.Append);
br.TranslateTransform(wUnit * wStep, hUnit * hStep, MatrixOrder.Append);
e.Graphics.FillRectangle(br, rTile);
br.ResetTransform();
}
}
}
}
private void Form1_Resize(object sender, EventArgs e)
{
this.Invalidate();
}
}
}
Here's a snapshot of the output:
I recommend you to create a generic method like this:
public void Paint_rectangle(object sender, PaintEventArgs e)
{
RectangleF r = new RectangleF(0, 0, e.ClipRectangle.Width, e.ClipRectangle.Height);
if (r.Width > 0 && r.Height > 0)
{
Color c1 = Color.LightBlue;
Color c2 = Color.White;
Color c3 = Color.LightBlue;
LinearGradientBrush br = new LinearGradientBrush(r, c1, c3, 90, true);
ColorBlend cb = new ColorBlend();
cb.Positions = new[] { 0, (float)0.5, 1 };
cb.Colors = new[] { c1, c2, c3 };
br.InterpolationColors = cb;
// paint
e.Graphics.FillRectangle(br, r);
}
}
then, for every rectangle just call:
yourrectangleF.Paint += new PaintEventHandler(Paint_rectangle);
If the gradrients colors are all the same, you can make that method shorter. Hope that helped..
I have an Image control in WPF which contains an image with lots of transparent pixels. Right now, the MouseDown event on Image fires whenever I click within the full rectangular region of the Image control. I would like some way to detect if the mouse click occurred on a nontransparent portion of the image.
What would be the best way of doing this?
Using the technique in this answer you can derive from Image to create an OpaqueClickableImage that only responds to hit-testing in sufficiently non-transparent areas of the image:
public class OpaqueClickableImage : Image
{
protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
{
var source = (BitmapSource)Source;
// Get the pixel of the source that was hit
var x = (int)(hitTestParameters.HitPoint.X / ActualWidth * source.PixelWidth);
var y = (int)(hitTestParameters.HitPoint.Y / ActualHeight * source.PixelHeight);
// Copy the single pixel into a new byte array representing RGBA
var pixel = new byte[4];
source.CopyPixels(new Int32Rect(x, y, 1, 1), pixel, 4, 0);
// Check the alpha (transparency) of the pixel
// - threshold can be adjusted from 0 to 255
if (pixel[3] < 10)
return null;
return new PointHitTestResult(this, hitTestParameters.HitPoint);
}
}
after adding this class, just use it like a regular image:
<utils:OpaqueClickableImage Name="image" Source="http://entropymine.com/jason/testbed/pngtrans/rgb8_t_bk.png" Stretch="None"/>
public class OpaqueClickableImage : Image
{
protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
{
var source = (BitmapSource)Source;
var x = (int)(hitTestParameters.HitPoint.X / ActualWidth * source.PixelWidth);
var y = (int)(hitTestParameters.HitPoint.Y / ActualHeight * source.PixelHeight);
if (x == source.PixelWidth)
x--;
if (y == source.PixelHeight)
y--;
var pixels = new byte[4];
source.CopyPixels(new Int32Rect(x, y, 1, 1), pixels, 4, 0);
return (pixels[3] < 1) ? null : new PointHitTestResult(this, hitTestParameters.HitPoint);
}
}
I have a custom WPF Canvas, upon which I would like to show a grid. I do so by overriding the OnRender method on Canvas, and using the DrawingContext drawing functions. IsGridVisible, GridWidth, GridHeight are the number of pixels between each grid line horizontally and vertically respectively.
I also use a ScaleTransform on the Canvas.LayoutTransform property to zoom the Canvas items and as one expects, the grid line thicknesses are multiplied by the ScaleTransform scaling factors as shown in the below image. Is there any way to draw single pixel lines, irrespective of the current Canvas RenderTransform?
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
base.OnRender(dc);
if (IsGridVisible)
{
// Draw GridLines
Pen pen = new Pen(new SolidColorBrush(GridColour), 1);
pen.DashStyle = DashStyles.Dash;
for (double x = 0; x < this.ActualWidth; x += this.GridWidth)
{
dc.DrawLine(pen, new Point(x, 0), new Point(x, this.ActualHeight));
}
for (double y = 0; y < this.ActualHeight; y += this.GridHeight)
{
dc.DrawLine(pen, new Point(0, y), new Point(this.ActualWidth, y));
}
}
}
alt text http://www.freeimagehosting.net/uploads/f05ad1f602.png
As the comments to the original post state. The Pen thickness should be set to 1.0/zoom.