How can I apply stroke to a textbox in WPF? - c#

I need to apply stroke (outline around text) to a textbox.
I have tried some solutions:
Edit the template of a textbox, using OutlinedTextblock (a custom control to draw the outlined text), but I can't select characters;
apply shader effect, but it didn't work well. The stroke looked ugly.
Do you have any good solution?

I did a bit of experimentation and a proof of concept.
The result isn't too bad but needs a bit of work.
When I select text the blue area isn't in exactly the right place.
This works using the approach I mentioned in comments.
The text in the textbox is transparent and the background of the control is used to show the formatted text.
My windows just has this in it:
<Grid>
<TextBox Width="200" Height="40"
VerticalAlignment="Top"
TextChanged="TextBox_TextChanged"
Foreground="Transparent"
FontSize="32"
>
<TextBox.Background>
<DrawingBrush Stretch="None"
AlignmentX="Left">
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="LightBlue"
x:Name="TextGeometryDrawing"
Geometry="{x:Null}"
>
<GeometryDrawing.Pen>
<Pen Thickness="1" Brush="Red"/>
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</TextBox.Background>
</TextBox>
</Grid>
My textchanged handler creates the geometry:
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox tb = (TextBox)sender;
FormattedText formattedText = new FormattedText(tb.Text, CultureInfo.GetCultureInfo("en-us"),
FlowDirection.LeftToRight, new Typeface("Segoe UI"), 32, Brushes.Black,
VisualTreeHelper.GetDpi(this).PixelsPerDip);
Geometry geom = formattedText.BuildGeometry(new System.Windows.Point(0, 0));
TextGeometryDrawing.Geometry = geom;
}
Looks like
An alternative is the approach I use in our map and scenario editors. I create a geometry per letter a-z and then use a horizontal itemscontrol with a path per letter using those geometries.
You can then control each letter precisely. If you don't like some aspect of what you get from truetype conversion you can edit it in inkscape.
The insertion point and selection is likely to get even more offset that way though.

Related

How to clip an element fron another one in WPF? [duplicate]

Anyone know a good way to create this object from Xaml? It also has to work at .5 Opacity when layered on top of other controls.
It also has to be resizable via Horizontal or Vertical Alignment.
I'm having some difficulty. The closest I get is with 2 borders, one having a negative margin--but it doesn't work when Opacity is applied.
Code that works:
<Path Fill="Black">
<Path.Data>
<CombinedGeometry GeometryCombineMode="Exclude">
<CombinedGeometry.Geometry1>
<RectangleGeometry RadiusX="5" RadiusY="5" Rect="0,0,200,100" />
</CombinedGeometry.Geometry1>
<CombinedGeometry.Geometry2>
<RectangleGeometry RadiusX="5" RadiusY="5" Rect="105,5,90,90" />
</CombinedGeometry.Geometry2>
</CombinedGeometry>
</Path.Data>
</Path>
Use a GeometryGroup with an EvenOdd FillRule, or a CombinedGeometry with a GeometryCombineMode of Xor or Exclude. The geometries to combine will both be RectangleGeometry objects, with an appropriate RadiusX and RadiusY. The result will be the outer rectangle with a "hole" in it where the inner rectangle was located. (I assume this is what you want rather than a white rectangle within the black one.)
You can then assign this composite geometry to a Path as its Data property, and set the Fill and Opacity as required.

How to override all Shapes in a specific area WPF

Currently I am filling my MainWindow with a slightly transparent black:
But I want it to have a "hole" where this effect doesn't take place which should look like the following:
So this needs to be done at runtime since the area which the hole represents is going to change multiple times while the program is running.
What I thought I could do
So at first I thought I could just cut the area in the middle out
like you could do with a Graphics object, but the slightly
transparent black is nothing but a rectangle which is added as a child on a canvas which is currently done like this:
var background = new System.Windows.Shapes.Rectangle
{
Fill = new SolidColorBrush(System.Windows.Media.Color.FromArgb(150, 0, 0, 0)),
Width = ScreenInfo.Width,
Height = ScreenInfo.Height
};
MainCanvas.Children.Add(background);
But I couldn't fine any way to achieve this cut effect.
Creating 4 Rectangles which would look something like this: but this way of doing it didn't seem to me as the most effecient way of achieving this.
Thanks for any kind of help!
Create a CombinedGeometry by cutting a smaller square out of a larger one and then use that with a path. How you size it will depend on your application, a Viewbox will probably be good enough for most cases:
<Grid>
<TextBlock Text="Hello World!" FontSize="200" Foreground="Red" TextWrapping="Wrap" TextAlignment="Center"/>
<Viewbox Stretch="UniformToFill">
<Path Fill="#C0000000">
<Path.Data>
<CombinedGeometry GeometryCombineMode="Exclude">
<CombinedGeometry.Geometry1>
<RectangleGeometry Rect="0,0,4,4" />
</CombinedGeometry.Geometry1>
<CombinedGeometry.Geometry2>
<RectangleGeometry x:Name="cutRect" Rect="1,1,2,2" />
</CombinedGeometry.Geometry2>
</CombinedGeometry>
</Path.Data>
</Path>
</Viewbox>
</Grid>
Then to change the size of the inner geometry you can either bind its Rect to a view model property or change it directly in code-behind:
cutRect.Rect = new Rect(1, 1, 1, 1);

Use a GeometryDrawing object to remove margins and set the size of a vector icon

I'm trying to build a GridView with a list of file objects and their properties. One of the properties is the logo of the fieldbus protocol, supported by the file.
After the protocol logos were converted from .png to .xaml, the current UI looks like the following:
Current UI state
The example of one .xaml logo file:
<?xml version="1.0" encoding="utf-8"?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<DrawingImage x:Key="EtherCAT_design">
<DrawingImage.Drawing>
<DrawingGroup>
<DrawingGroup.Children>
<GeometryDrawing Brush="#FFFEFEFE"
Geometry="F1 M0,601.33335 L0,0 947.33335,0 1894.6667,0 1894.6667,601.33335 1894.6667,1202.6667 947.33335,1202.6667 0,1202.6667 z" />
<GeometryDrawing Brush="..."
Geometry="..." />
<GeometryDrawing Brush="..."
Geometry="..." />
...
...
</DrawingGroup.Children>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</ResourceDictionary>
Now, I would like to remove the margins and make the background transparent for each logo on the list.
What I've already tried:
Removing the first GeometryDrawing from the DrawingGroup.Children produces different default sizes for each picture (Removing the first GeometryDrawing)
Setting another value for the Geometry property of the first GeometryDrawing object: Geometry="F1M16,16L0,16 0,0 16,0z" still leaves a margin above the picture (Adjusting the Geometry property)
After reading the MSDN page it is still unclear to me, how to define the Geometry property so that I wouldn't have any margins and still have all the pictures of the same size on the list.
Apparently the first geometry is an overcomplicated version of the rectangle (0, 0, 1894.6667, 1202.6667).
Assuming that the center of the icon is at the center of the "frame" rectangle, i.e. at about (947, 601), you may simply create a smaller rectangle centered at the same point. Put that into a transparent GeometryDrawing:
<GeometryDrawing Brush="Transparent">
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="547,201,800,800"/>
</GeometryDrawing.Geometry>
</GeometryDrawing>
The example above define a 800 x 800 rectangle, still centered at (947, 601)

Two Image Layers and OpacityMask

I'm trying to crop a circle from one image, and put it on top another image in WPF.
The Circle's center changes according to the mouse movements, and needs to be bounded dynamically.
I tried to position two images on top of each other, and use a third image that I draw in real time as an opacity mask.
Could you please provide short code to solve this problem efficiently ?
The code below describes what you can do with an OpacityMask. It's a little counterintuitive, because we expect a XAML rendering to layer elements bottom-to-top.
However, in this case you want your "background" image to layer on top of the foreground, because the OpacityMask will serve to display only that portion of the foreground described by the position and size of the VisualBrush, rendering the rest transparent. It's given as follows:
<Grid x:Name="MainGrid" MouseMove="Grid_MouseMove">
<Rectangle Fill="Red" ></Rectangle>
<Rectangle Fill="Green">
<Rectangle.OpacityMask>
<VisualBrush Stretch="None" >
<VisualBrush.Visual>
<Ellipse Width="40" Height="40" StrokeThickness="1" Fill="Black" />
</VisualBrush.Visual>
<VisualBrush.RelativeTransform>
<TransformGroup>
<TranslateTransform x:Name="OpacityFilterTransform" X="1" Y="1"/>
</TransformGroup>
</VisualBrush.RelativeTransform>
</VisualBrush>
</Rectangle.OpacityMask>
</Rectangle>
</Grid>
Then, this event handler code computes the position of the ellipse and applies it to the OpacityFilter's TranslateTransform object, giving you control over the position of the image.
private void Grid_MouseMove(object sender, MouseEventArgs e)
{
var position = e.GetPosition(this);
var height = MainGrid.ActualHeight;
var width = MainGrid.ActualWidth;
// with the position values, interpolate a TranslateTransform for the opacity mask
var transX = position.X / width;
var transY = position.Y / height;
OpacityFilterTransform.X = transX - 0.5;
OpacityFilterTransform.Y = transY - 0.5;
}
This solution should work for any descendant of Visual you care to layer.

Why is a GeometryDrawing displayed on Canvas with clipped coordinates?

I have the following simple code that draws rectangle
<Canvas Name="MainImageLayer" >
<Image >
<Image.Source >
<DrawingImage xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" >
<DrawingImage.Drawing >
<DrawingGroup>
<DrawingGroup.Children>
<GeometryDrawing>
<GeometryDrawing.Pen>
<Pen Brush="#FF1acc33" Thickness="1" />
</GeometryDrawing.Pen>
<GeometryDrawing.Brush>
<SolidColorBrush>Red</SolidColorBrush>
</GeometryDrawing.Brush>
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="300,480,287,83" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup.Children>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
</Canvas>
The result looks like this - note that the rectangle is in (0,0), even that the Rect is defined as
<RectangleGeometry Rect="300,480,287,83" />
I want it to start at (300,480), like that:
I can achieve that by inspecting my DrawingImage and doing :
<Canvas.Top>300</Canvas.Top>
<Canvas.Left>480</Canvas.Left>
But isn't there a better way, considering the fact that this data is encoded into the Geometry?
Your problem comes from the fact that you have all of your geometry wrapped in an 'Image' object. By default, the .Height and .Width properties of an Image object are set to 'Auto', and the .Stretch property is set to 'Uniform'. This guarantees that your rectangle will always appear in the top left corner of your Canvas.
If you really need to encapsulate your geometry in an 'Image' object (which I would caution you not to do) you will need to set the Margin of your Image object to 300,480,0,0. in order to get your rectangle to appear where you want it. This is required because of the way an 'Image' object handles its contents.
An Image object does not behave like a Canvas object, even if it is inside one.
Unless there is some overwhelming reason to keep the Image object, you will have much better success if you discard the Image and draw directly on the Canvas.
EDIT
Why should an Image object not be used in this case?
An Image object is primarily used for displaying...well, images, like bitmaps and such. It is not suited for drawing geometry in it at a specific location (and size). Like most WPF controls, it is what I would call a 'relative' control, meaning it is well suited for automatically resizing and positioning itself in relation to both its contents and its parent. A Canvas on the other hand is an example of an 'absolute' control. Its entire reason for existence is to allow content to be drawn upon it in an exact location with an exact size. Adding an Image inside a Canvas and then drawing the geometry inside the Image just adds an unnecessary layer of complexity between the Canvas and the geometry that needs to be drawn.
How can the geometry be added directly?
One of the easiest ways would be to use the Path object given in Clemens' answer. Just replace your entire Image object and all its contents with the 5 lines of that Path and your rectangle will appear exactly where it should be. You can also do it with a single line and a Rectangle object:
<Rectangle Height="83" Width="287" Margin="300,480,0,0" Stroke="#FF1acc33" StrokeThickness="1" Fill="Red" />
but I would recommend the Path since it contains the size and position of the rectangle in one set of numbers. The Path also allows you much more flexibility if you are working with shapes other than rectangles.
Seems like the DrawingImage (or the GeometryDrawing?) is somehow adjusted to its visible rectangle. Anyway, wouldn't this be a much simpler solution, instead of Image?
<Path Stroke="#FF1acc33" StrokeThickness="1" Fill="Red">
<Path.Data>
<RectangleGeometry Rect="300,480,287,83" />
</Path.Data>
</Path>
You can also add another non-empty GeometryDrawing that covers (0,0) (e.g. a RectangeGeometry with Rect="0,0,1,1") with a transparent brush to the DrawingGroup.
EDIT: As i understood, the DrawingImage is adjusted to Drawing.Bounds

Categories

Resources