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);
}
}
Related
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 trying to set zooming and panning limits on a control I found here:
https://wpfextensions.codeplex.com
I managed to set zooming limits, but now I'm having trouble setting the panning limits so that you can't pan the object inside the canvas, outside the view.
I succeeded in setting the limits, but only when the zoom level is 1 (Zoom == 1, so no zoom), but the moment you increase the zoom (by rotating the mouse wheel) things start to go wrong: the limits are set, but they are not set correctly.
In order to set them correctly I have to take into consideration the deltaZoom (the amount zoom changed compared to the previous zoom value).
Small demo project
I have created a simple, standalone project where I can reproduce the issue:
https://github.com/igorpopovio/CanvasZoomPan
The project shows a desktop window with the ZoomControl (canvas with ScaleTransform, TranslateTransform and a bunch of dependency properties to make it easier to work with the transforms). The ZoomControl contains a red square and the window contains the ZoomControl and a debug list of properties so I can see live how they change based on left click drag and mouse wheel zoom.
Expected vs actual behaviour
Expected behaviour: object/red square edge cannot get out of the current view.
Actual behaviour: object/red square edge gets out of the current view (still has limits, but aren't correctly set).
Code explanations
All the action happens in this file and the important bits are:
the panning limits: MinTranslateX, MaxTranslateX; MinTranslateY, MaxTranslateY
the current panning: TranslateX, TranslateY
the current zoom: Zoom
the amount zoom changed: deltaZoom (local variable)
the Zoom_PropertyChanged method
the LimitZoomingAndPanning method
What I tried
In the LimitZoomingAndPanning method I set the translation/panning limits which are working for Zoom == 1 (deltaZoom == 1), but are giving incorrect limits for any other Zoom values:
MinTranslateX = box.BottomLeft.X * deltaZoom;
MinTranslateY = box.BottomLeft.Y * deltaZoom;
MaxTranslateX = ActualWidth - box.Size.Width * deltaZoom;
MaxTranslateY = ActualHeight - box.Size.Height * deltaZoom;
The box variable is actually the bounding box of the object inside the canvas. ActualWidth and ActualHeight are the size of the canvas on which the object is rendered.
Logically, all the translation/panning limits should depend on deltaZoom.
Maybe I'm missing something?
I originally tried doing the same thing with the ZoomAndPanControl but wasn't able to achieve what I wanted so I ended up writing my own control to provide a constrained zoom and pan control.
I have packaged this control up on nuget but it can be found on my gist and on my github with a demo project loading an image into the viewport control.
PM > Install-Package Han.Wpf.ViewportControl
Usage:
<controls:Viewport MinZoom="1" MaxZoom="50" ZoomSpeed="1.1">
<Grid width="1200" height="1200">
<Button />
</Grid>
</controls:Viewport
and add the theme to the app.xaml:
<Application.Resources>
<ResourceDictionary Source="pack://application:,,,/Han.Wpf.ViewportControl;component/Themes/Generic.xaml" />
</Application.Resources>
I know your not using the MatrixTransform but to constrain the control to the bounds of the parent is calculated like below:
private void OnMouseWheel(object sender, MouseWheelEventArgs e)
{
if (IsEnabled)
{
var scale = e.Delta > 0 ? ZoomSpeed : 1 / ZoomSpeed;
var position = e.GetPosition(_content);
var x = Constrain(scale, MinZoom / _matrix.M11, MaxZoom / _matrix.M11);
var y = Constrain(scale, MinZoom / _matrix.M22, MaxZoom / _matrix.M22);
_matrix.ScaleAtPrepend(x, y, position.X, position.Y);
ZoomX = _matrix.M11;
ZoomY = _matrix.M22;
Invalidate();
}
}
private void OnMouseMove(object sender, MouseEventArgs e)
{
if (IsEnabled && _capture)
{
var position = e.GetPosition(this);
var point = new Point
{
X = position.X - _origin.X,
Y = position.Y - _origin.Y
};
var delta = point;
_origin = position;
_matrix.Translate(delta.X, delta.Y);
Invalidate();
}
}
In the Invalidate call Constrain()
private double Constrain(double value, double min, double max)
{
if (min > max)
{
min = max;
}
if (value <= min)
{
return min;
}
if (value >= max)
{
return max;
}
return value;
}
private void Constrain()
{
var x = Constrain(_matrix.OffsetX, _content.ActualWidth - _content.ActualWidth * _matrix.M11, 0);
var y = Constrain(_matrix.OffsetY, _content.ActualHeight - _content.ActualHeight * _matrix.M22, 0);
_matrix = new Matrix(_matrix.M11, 0d, 0d, _matrix.M22, x, y);
}
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));
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.
I am trying to position an Adorner depending on the dimensions of the parent of the adorned element. For example, I have a textbox. I want to adorn this textbox so it looks something like this:
how the adorner needs to be placed http://img707.imageshack.us/img707/9840/fig1.png
A textbox is placed in a canvas object and if there is enough space available then place the adorner (semi transparent rounded square) in line with the bottom edge of the textbox. The adorner is initiated when the user clicks on the textbox.
Currently the canvas and its contents (the textbox) is hosted in a WinForms form - so the WPF is handled by the ElementHost control.
But when I run my code, when the textbox is clicked for the first time it displays the adorner aligned to the top edge of the textbox (see figure below). After that it positions itself correctly (like the figure above) Does anyone know why this might be?
how adorner is positions http://img14.imageshack.us/img14/4766/fig2v.png
I have pasted the code for this below:
TextBoxAdorner.cs - this the adorner logic
public class TextBoxAdorner : Adorner
{
private TextBox _adornedElement;
private VisualCollection _visualChildren;
private Rectangle _shape;
private Canvas _container;
private Canvas _parentCanvas;
public TextBoxAdorner(UIElement adornedElement, Canvas parentCanvas)
: base(adornedElement)
{
_adornedElement = (TextBox)adornedElement;
_parentCanvas = parentCanvas;
_visualChildren = new VisualCollection(this);
_container = new Canvas();
_shape = new Rectangle();
_shape.Width = 100;
_shape.Height = 80;
_shape.Fill = Brushes.Blue;
_shape.Opacity = 0.5;
_container.Children.Add(_shape);
_visualChildren.Add(_container);
}
protected override Size ArrangeOverride(Size finalSize)
{
Point location = GetLocation();
_container.Arrange(new Rect(location, finalSize));
return finalSize;
}
private Point GetLocation()
{
if (_parentCanvas == null)
return new Point(0, 0);
Point translate;
double xloc = 0, yloc = _shape.Height - _adornedElement.ActualHeight;
if (yloc < 0) // textbox is bigger than the shape
yloc = 0;
else
{
translate = this.TranslatePoint(new Point(0, -yloc), _parentCanvas);
// coordinate is beyond the position of the parent canvas
if (translate.Y < 0) // this is true the first time it's run
yloc = 0;
else
yloc = -yloc;
}
translate = this.TranslatePoint(new Point(_shape.Width, 0), _parentCanvas);
// textbox is in right edge of the canvas
if (translate.X > _parentCanvas.ActualWidth)
{
double pos = translate.X - _parentCanvas.ActualWidth;
translate = this.TranslatePoint(new Point(-pos,0), _parentCanvas);
if (translate.X < 0)
xloc = 0;
else
xloc = translate.X;
}
return new Point(xloc, yloc);
}
protected override Size MeasureOverride(Size constraint)
{
Size myConstraint = new Size(_shape.Width, _shape.Height);
_container.Measure(myConstraint);
return _container.DesiredSize;
}
protected override Visual GetVisualChild(int index)
{
return _visualChildren[index];
}
protected override int VisualChildrenCount
{
get
{
return _visualChildren.Count;
}
}
}
The position of an Adorner is relative to the adorned element. If you want it to be to the top of your object, the value of yloc should be negative. However, the code you have also regards the boundaries of the Canvas. If there's not enough place for the rectangle above, it would put it below. Trying placing the TextBox a little lower in the Canvas.