Adorner in WPF, isEnabled not working? - c#

I'm using an adorner layer to make a grid (like, boxes across the entire screen) across my grid (WPF grid).
I want this to be shown ONLY when a checkbox is marked. However, when I bind the "IsEnabled" property, nothing happens. Even if I set the IsEnabled to false, the grid overlay is still shown!
This is how I do from my mainWindow.xaml, note the IsEnabled is set to false, but it is still showing up:
<Grid>
<!--Adornerdecorator is made to make sure the adorner is in the background, and not the foreground-->
<AdornerDecorator>
<View:GridWithRulerxaml IsEnabled="False" />
</AdornerDecorator>
<ItemsControl ItemsSource="{Binding Classes}"/>
<ItemsControl ItemsSource="{Binding Edges}"/>
</Grid>
this is the adorner usercontrol:
namespace UMLDesigner.View
{
/// <summary>
/// Interaction logic for GridWithRulerxaml.xaml
/// </summary>
public partial class GridWithRulerxaml : UserControl
{
public GridWithRulerxaml()
{
InitializeComponent();
//Loaded event is necessary as Adorner is null until control is shown.
Loaded += GridWithRulerxaml_Loaded;
}
void GridWithRulerxaml_Loaded(object sender, RoutedEventArgs e)
{
var adornerLayer = AdornerLayer.GetAdornerLayer(this);
var rulerAdorner = new AlignmentAdorner(this);
adornerLayer.Add(rulerAdorner);
}
}
}
And finally the adorner itself:
namespace UMLDesigner.Utilities
{
public class AlignmentAdorner : Adorner
{
private FrameworkElement element;
public AlignmentAdorner(UIElement el)
: base(el)
{
element = el as FrameworkElement;
}
protected override void OnRender(System.Windows.Media.DrawingContext drawingContext)
{
base.OnRender(drawingContext);
double height = element.ActualHeight;
double width = element.ActualWidth;
double linesHorizontal = height / 50;
double linesVertical = width / 50;
var pen = new Pen(Brushes.LightGray, 1) { StartLineCap = PenLineCap.Triangle, EndLineCap = PenLineCap.Triangle };
int offset = 0;
for (int i = 0; i <= linesVertical; ++i)
{
offset = offset + 50;
drawingContext.DrawLine(pen, new Point(offset, 0), new Point(offset, height));
}
offset = 0;
for (int i = 0; i <= linesHorizontal; ++i)
{
offset = offset + 50;
drawingContext.DrawLine(pen, new Point(0, offset), new Point(width, offset));
}
}
}
}
I really hope you can help me out here guys.

The IsEnabled property only enables/disables interaction with the element, and has nothing to do with the Visibility.
What you should do is set the Visibility property of the GridWithRulerxaml to Collapsed or Hidden when you want to hide it, and set it to Visible when you want it shown.
Edit: Tested it, setting the visiblity to Hidden of the GridWithRulerxaml usercontrol inside the AdornerDecorator doesn't hide the adorner.
And thinking some more about it, if this isn't what you want, it might be possible to do this with the IsEnabled property, watching for changes in it and adding/removing the adorner depending on the value.

Related

What's the best way to draw chart with discrete values on Canvas?

I have a Canvas with dynamic width. I need to draw a chart on this Canvas. Due to the amount of the measured values and the business area specificity it's not possible to use third-party controls and components.
The chart shows the level of some measurement that is discrete. The X axis indicates the time of the measurement. In the worst case every pixel of the chart can have different level. So it looks like this:
My first approach just to make it working was to draw a line for every pixel. So my code looks like this:
MyCanvas.Children.Clear();
var random = new Random();
for (var i = 0; i < MyCanvas.Width; i++)
{
var line = new Line()
{
X1 = i,
X2 = i,
Y1 = MyCanvas.ActualHeight,
Y2 = MyCanvas.ActualHeight - random.Next(0, (int)MyCanvas.ActualHeight),
Stroke = Brushes.Blue
};
MyCanvas.Children.Add(line);
}
This code does what I want it to do. It draws a chart like this one:
But it seems that it is not an optimal way to do things like this one. My chart should support panning and zooming and it takes around 200-350ms to redraw a chart on every user request. That's too much (1000/350 = 2.85fps).
I don't have much experience in WPF so my question is - what is the most optimal way to draw such chart? Maybe I need to use Paths and Geometry objects but I can't say for sure that it will give much perfomance gain until I implement it. Also I don't know what kind of Geometry I need to use. It seems that LineGeometry matches my expectations.
Thanks in advance.
I'll have another shot at answering this, without data binding this time. Given the need for high performance the best approach would probably be to use a custom FrameworkElement. Here's an example that draws 10,000 samples and is able to maintain 60fps on my laptop, with each sample being updated once per frame:
public class FastChart : FrameworkElement
{
private Pen ChartPen;
private const int MaxSampleVal = 1000;
private const int NumSamples = 10000;
private int[] Samples = new int[NumSamples];
public FastChart()
{
this.ChartPen = new Pen(Brushes.Blue, 1);
if (this.ChartPen.CanFreeze)
this.ChartPen.Freeze();
ClipToBounds = true;
this.SnapsToDevicePixels = true;
CompositionTarget.Rendering += CompositionTarget_Rendering;
var rng = new Random();
for (int i = 0; i < NumSamples; i++)
this.Samples[i] = MaxSampleVal / 2;
}
private void CompositionTarget_Rendering(object sender, EventArgs e)
{
// update all samples
var rng = new Random();
for (int i = 0; i < NumSamples; i++)
this.Samples[i] = Math.Max(0, Math.Min(MaxSampleVal, this.Samples[i] + rng.Next(11) - 5));
// force an update
InvalidateVisual();
}
protected override void OnRender(DrawingContext drawingContext)
{
// hacky clip to parent scroller
var minx = 0;
var maxx = NumSamples;
var scroller = this.Parent as ScrollViewer;
if (scroller != null)
{
minx = Math.Min((int)scroller.HorizontalOffset, NumSamples - 1);
maxx = Math.Min(NumSamples, minx + (int)scroller.ViewportWidth);
}
for (int x = minx; x < maxx; x++)
drawingContext.DrawLine(this.ChartPen, new Point(0.5 + x, 1000), new Point(0.5 + x, 1000 - this.Samples[x]));
}
}
A few things to note here:
the pen you use to render the lines needs to be frozen, failing to do this will result in a huge performance decrease.
if your chart is inside a ScrollViewer (say) you'll want to clip to the visible portion of the screen. In my example above I've just hacked it in by checking if the parent is a ScrollViewer, you may need to do this differently depending on your circumstances.
as you can see, I'm subscribing to CompositionTarget.Rendering to update the chart values and force a render. this is known to sometimes be called more than once per frame, but there are workarounds for this so I'll leave it to you to find one.
In any case, here's an example of how you would use this control (I've hard-coded the width here, obviously you'd want to binding it to a property in your view model or something):
<ScrollViewer x:Name="theScrollViewer" HorizontalScrollBarVisibility="Auto">
<controls:FastChart Width="10000" />
</ScrollViewer>
And here's a still from the (animating) result:
The only problem with this is that it doesn't use Canvas, but I think in order to get the performance you want you're going to have to ditch that requirement.
Very easy to do, but I'd strongly recommend you use data binding.
Create an ItemsControl.
Give the ItemsControl an ItemsPanel of type Canvas.
Bind the Canvas Width and Height to elements in your view model specifying the extents of each axis.
Wrap the whole thing in a Viewbox so that it scales automatically, irrespective of width and height.
For each element in your ItemsControl list create a view model containing X1/Y1/X2/Y2, then use a DataTemplate to render those items using those values to draw a vertical line.
So basically your main view model should look something like this (which by way of demonstration is generating random data points):
public class MainViewModel : ObservableObject
{
private IEnumerable<DataPointViewModel> _DataPoints;
public IEnumerable<DataPointViewModel> DataPoints
{
get { return this._DataPoints; }
set { Set(() => this.DataPoints, ref this._DataPoints, value); }
}
public int ChartWidth { get; } = 1000;
public int ChartHeight { get; } = 1000;
public MainViewModel()
{
var rng = new Random();
this.DataPoints = Enumerable.Range(0, this.ChartWidth)
.Select(x => new DataPointViewModel {X1 = x, Y1 = this.ChartHeight-1, X2 = x, Y2 = this.ChartHeight - 1 - rng.Next(this.ChartHeight) });
}
}
The view model for the items in your collection need to provide the points for the line that is used to render them:
public class DataPointViewModel : ObservableObject
{
private double _X1;
public double X1
{
get { return this._X1; }
set { Set(() => this.X1, ref this._X1, value); }
}
private double _Y1;
public double Y1
{
get { return this._Y1; }
set { Set(() => this.Y1, ref this._Y1, value); }
}
private double _X2;
public double X2
{
get { return this._X2; }
set { Set(() => this.X2, ref this._X2, value); }
}
private double _Y2;
public double Y2
{
get { return this._Y2; }
set { Set(() => this.Y2, ref this._Y2, value); }
}
}
And then your XAML simply uses these to render a line for each data point on a Canvas control:
<Viewbox>
<ItemsControl ItemsSource="{Binding DataPoints}" Width="{Binding ChartWidth}" Height="{Binding ChartHeight}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Line Stroke="Blue" StrokeThickness="1" X1="{Binding X1}" Y1="{Binding Y1}" X2="{Binding X2}" Y2="{Binding Y2}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Viewbox>
This will all in turn generate the following output:

TextBlock in UserControl not displaying text

I'm trying to create a Nonogram (aka PuzzleCross) puzzle grid in C#/WPF, and have created two UserControls to contain the row and column keys. Each UserControl consists of a Border containing a TextBlock, with a DependencyProperty named TextControl to make the Text property accessible outside of the UserControl. Everything works fine except that the text isn't actually displayed when run. The TextControl contains the correct text, as tested with a MouseDown event and a MessageBox, but for some reason the text just isn't there.
Can anyone help me figure out what I'm missing? I have a feeling it's a simple thing, but I'm just not seeing it.
Horizontal UserControl:
<Border BorderBrush="Black" BorderThickness="1" HorizontalAlignment="Center" Height="10" Width="100">
<TextBlock Text="{Binding ElementName=HorizontalRowLabel, Path=TextContent}" Foreground="Black" FontSize="6" MouseDown="TextBlock_MouseDown"/>
</Border>
Horizontal C#:
public partial class HorizontalRowLabel : UserControl
{
public static readonly DependencyProperty TextContentProperty = DependencyProperty.Register("TextContent", typeof(string),
typeof(HorizontalRowLabel), new FrameworkPropertyMetadata(""));
public string TextContent
{
get { return (string)GetValue(TextContentProperty); }
set { SetValue(TextContentProperty, value); }
}
private void TextBlock_MouseDown(object sender, MouseButtonEventArgs e)
{
MessageBox.Show(TextContent);
}
public HorizontalRowLabel()
{
InitializeComponent();
}
}
//Adds text HorizontalRowLabel UserControl, then adds HRL to Grid.
public void InitRowKeys(Grid puzzle)
{
for(int i = 0; i < HorizontalKeys.Length; i++)
{
RowDefinition row = new RowDefinition();
HorizontalRowLabel hrow = new HorizontalRowLabel();
row.Height = new GridLength(10);
for(int j = 0; j < HorizontalKeys[i].Length; j++)
{
if(HorizontalKeys[i].Length == 0 || j == HorizontalKeys[i].Length - 1)
{
hrow.TextContent += HorizontalKeys[i][j].ToString();
hrow.Foreground = Brushes.Black;
hrow.SetValue(Grid.RowProperty, i);
hrow.SetValue(Grid.ColumnProperty, 0);
hrow.FontSize = 6;
hrow.HorizontalAlignment = HorizontalAlignment.Right;
hrow.VerticalAlignment = VerticalAlignment.Center;
}
else
{
hrow.TextContent += HorizontalKeys[i][j].ToString() + " ";
hrow.SetValue(Grid.RowProperty, i);
hrow.SetValue(Grid.ColumnProperty, 0);
hrow.FontSize = 6;
hrow.HorizontalAlignment = HorizontalAlignment.Right;
hrow.VerticalAlignment = VerticalAlignment.Center;
}
}
//puzzle.Margin = new Thickness(0,50,0,0);
hrow.Width = 100;
hrow.Height = 30;
puzzle.RowDefinitions.Add(row);
puzzle.Children.Add(hrow);
}
}
A Binding like
Text="{Binding ElementName=HorizontalRowLabel, Path=TextContent}"
only works if you have assigned the x:Name attribute to the UserControl:
<UserControl ... x:Name="HorizontalRowLabel">
...
</UserControl>
That is however not necessary with a RelativeSource Binding:
Text="{Binding TextContent, RelativeSource={RelativeSource AncestorType=UserControl}}"

Rectangle event handler not working unless Fill property is set: why?

I declared a UniformGrid in my XAML file:
<UniformGrid Rows="8" Columns="8" Background="OliveDrab"
Name="board" Width="400" Height="400"/>
Then, I would like to programmatically add Rectangle children to it, and attach an event handler to each of them:
public MainWindow()
{
InitializeComponent();
createGrid();
}
private void createGrid()
{
SolidColorBrush scb = Brushes.Olive;
for (int i = 0; i < 64; i++)
{
Rectangle r = new Rectangle();
r.Stroke = scb;
r.MouseLeftButtonDown += Rectangle_MouseLeftButtonDown;
board.Children.Add(r);
}
}
private void Rectangle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Console.WriteLine("Rectangle_MouseLeftButtonDown");
}
But when I run the app and click on a Rectangle, the Rectangle_MouseLeftButtonDown is never called, unless I set a Fill property:
private void createGrid()
{
SolidColorBrush scb = Brushes.Olive;
SolidColorBrush whiteScb = Brushes.White;
for (int i = 0; i < 64; i++)
{
Rectangle r = new Rectangle();
r.Stroke = scb;
r.Fill = whiteScb;
r.MouseLeftButtonDown += Rectangle_MouseLeftButtonDown;
board.Children.Add(r);
}
}
Then and only then, the event is triggered.
So my question is: why must the Fill property of a Rectangle be set in order to make the event handler work?
Thanks.
Edit: as King King pointed, it looks like transparent windows don't send mouse events. More info: 'Transparent Windows in WPF' on msdn.
This is because you don't set the Fill property, the Rectangle's background will be transparent, and transparent controls are also "transparent" to mouse click. Hit testing goes straight to the first non-transparent control under it.
If you want to get a transparent rectangle which can be clicked then try
r.Fill = new SolidColorBrush(Colors.Black);
r.Opacity = 0;
However, you can set the Rectangle's Fill property to Transparent and it will pass on hit test and send the needed mouse events while Rectangle remains transparent.

Custom panel with layout engine

I'm trying to create a custom Panel control with my own layout engine.
I need every control that is added to my panel to be added below and to take full width (-padding), like below:
Below is my code:
using System.Drawing;
using System.Windows.Forms;
using System.Windows.Forms.Layout;
namespace VContainer
{
internal class VerticalFillList : Panel
{
public VerticalFillList()
{
AutoScroll = true;
MinimumSize = new Size(200, 200);
Size = new Size(200, 300);
Padding = new Padding(10);
}
private readonly VerticalFillLayout _layoutEngine = new VerticalFillLayout();
public override LayoutEngine LayoutEngine
{
get { return _layoutEngine; }
}
private int _space = 10;
public int Space
{
get { return _space; }
set
{
_space = value;
Invalidate();
}
}
}
internal class VerticalFillLayout : LayoutEngine
{
public override bool Layout(object container, LayoutEventArgs layoutEventArgs)
{
var parent = container as VerticalFillList;
Rectangle parentDisplayRectangle = parent.DisplayRectangle;
Point nextControlLocation = parentDisplayRectangle.Location;
foreach (Control c in parent.Controls)
{
if (!c.Visible)
{
continue;
}
c.Location = nextControlLocation;
c.Width = parentDisplayRectangle.Width;
nextControlLocation.Offset(0, c.Height + parent.Space);
}
return false;
}
}
}
Above code works fine, except one thing:
when I'm adding controls to my container they are added correctly (new below parent, 100% width), but when height of controls is bigger than my container height I get horizontal scrollbars, but after adding couple controls more scrollbar is removed.
Same thing happens when I want to resize my container:
How this can be fixed? I just need to remove that horizontal scrollbar.
Of course all improvements are welcome :)
I don't want to use table layout or flow layout as this one gives me exactly when I need.
I need a simple container that orders all child controls from top to bottom and stretches them horizontally so they take as much width so container horizontal scrollbar isn't needed.
Here is a working example that unfortunately, does not use your Layout Engine class. It simply relies on the OnControlAdded and OnControlRemoved methods, and anchoring and setting the AutoScrollMinSize property to specifically make sure the horizontal scrollbar never appears:
internal class VerticalPanel : Panel {
private int space = 10;
public int Space {
get { return space; }
set {
space = value;
LayoutControls();
}
}
protected override void OnControlAdded(ControlEventArgs e) {
base.OnControlAdded(e);
LayoutControls();
}
protected override void OnControlRemoved(ControlEventArgs e) {
base.OnControlRemoved(e);
LayoutControls();
}
private void LayoutControls() {
int height = space;
foreach (Control c in base.Controls) {
height += c.Height + space;
}
base.AutoScrollMinSize = new Size(0, height);
int top = base.AutoScrollPosition.Y + space;
int width = base.ClientSize.Width - (space * 2);
foreach (Control c in base.Controls) {
c.SetBounds(space, top, width, c.Height);
c.Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right;
top += c.Height + space;
}
}
}
You can set the AnchorProperty at you Buttons like:
button1.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top;
So they'll be resized horizontally

WPF: what is the right way to display a 2D array in a user-drawn control

I want to make a user-drawn control for the only purpose of displaying a Color[,] array. The control itself should draw an NxM grid of rectangles of corresponding colors.
I'm trying to inherit from a FrameworkElement and to override OnRender method:
public class CustomControl1 : FrameworkElement
{
static CustomControl1()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomControl1), new FrameworkPropertyMetadata(typeof(CustomControl1)));
}
public Color[,] ColorCollection
{
get { return (Color[,])GetValue(ColorGridProperty); }
set { SetValue(ColorGridProperty, value); }
}
public static readonly DependencyProperty ColorGridProperty =
DependencyProperty.Register("ColorCollection", typeof(Color[,]), typeof(CustomControl1),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));
protected override void OnRender(DrawingContext drawingContext)
{
if (ColorCollection != null)
{
int dimx = this.ColorCollection.GetLength(0);
int dimy = this.ColorCollection.GetLength(1);
double w = this.ActualWidth / dimx;
double h = this.ActualWidth / dimy;
for (int x = 0; x < dimx; x++)
{
for (int y = 0; y < dimy; y++)
{
SolidColorBrush brush = new SolidColorBrush(ColorCollection[x, y]);
drawingContext.DrawRectangle(brush, null, new Rect(x * w, 0, w, this.ActualHeight));
}
}
}
}
}
The problem is that my control doesn't redraw itself when i change elements in the underlying array. It works fine when i assign a whole new array or resize the control though.
Obviously I need another class which somehow notifies control about internal changes in the collection. I was looking at INotifyCollectionChange and ObservableCollection but the only articles I found were about binding collections to existing controls, not custom user-drawn ones. So I got confused and stuck at this point.
You probably can create a custom collection for yourself that works like your 2D array, but you need to also implement INotifyCollectionChange interface, which is not so hard to implement. That way WPF will listen to your collection changes and update the control if necessary.
I think the sample may be useful.

Categories

Resources