WPF set new control position relative within a grid - wpf

This is somewhat a duplicate of other questions so apologies in advance but I haven't been able to make sense of the existing answers (probably because I'm a WPF newb).
I have a grid within a canvas. The grid is added programmatically, not in xaml and is much smaller than the canvas. I want to programmatically add a control (text box) at the position the user clicks on the grid. The application may or may not be full screen and users screen resolutions may differ.
Currently I'm using a mouse down event and getting a point:
Dim p As Point = Mouse.GetPosition(myGrid)
And then using the point.x and point.y with Canvas.SetLeft and Canvas.SetTop but this only works when the app is full screen and the screen res is consistent.
I know it's bad form to ask for code but please include a snippet in your answer as I've been wrestling with this for some time & going round in circles. I'm using VB but answers in any language will be welcome. Thanks very much.

Grid arranges its children basically mostly on the child's Margin property.
So do this OnClick of your grid:
// dont forget to add an event handler on creating the grid
Grid myGrid = new Grid();
myGrid.MouseDown += myGrid_MouseDown;
private void myGrid_MouseDown(object sender, MouserEventArgs e)
{
Point p = e.GetPosition(myGrid);
TextBox tb = new TextBox();
tb.Margin = new Thickness(p.X, p.Y, 0, 0);
tb.HorizontalAlignment = HorizontalAlignment.Left;
tb.VerticalAlignment = VerticalAlignment.Top; // cuz we set margin on Top and Left sides..
myGrid.Children.Add(tb);
}
Hope it helps :)

I think that the problem is not on your code but in your xaml.
Mouse.GetPosition(myGrid)
should works well.
I think that your Grid is not the same size as your Canvas.
Try something like this:
<DockPanel>
<Canvas x:Name="can">
<Grid Height="{Binding ElementName=can, Path=ActualHeight}" Width="{Binding ElementName=can, Path=ActualWidth}" Background="Red" PreviewMouseDown="Grid_PreviewMouseDown" />
</Canvas>
</DockPanel>

Related

Programmatically position control in WPF

I am creating a karaoke program in WPF. Basically, I have a MediaElement which plays vidioes and such, and a StackPanel atop of that which I use to render stuff on top of the MediaElement.
I am trying to programmatically add a TextBlock to the StackPanel, which is going display the lyrics. The problem is that the TextBlock ends up in the top left corner no matter what I write.
Private LyricLabel As New TextBlock
Sub New(Panel As StackPanel)
With LyricLabel
.Foreground = Brushes.White
.FontFamily = New FontFamily("Verdana")
.FontSize = 20
.HorizontalAlignment = HorizontalAlignment.Stretch
.VerticalAlignment = VerticalAlignment.Bottom
End With
Panel.Children.Add(LyricLabel)
End Sub
Also, I want a ball or something to jump from word to word. Is there a easy way to get the width of each of the words + the space between them, or do I have to calculate that by myself?
It would be a better option to use a Grid and do that entirely in XAML. The Grid has the property that if many elements are in the same cell, they all overlap. So you put the MediaElement and a TextBlock together, with proper alignments and you're done:
<Grid>
<MediaElement/>
<TextBlock Text="{Binding CurrentLyric}" FontSize="20" HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0 0 0 30"/>
</Grid>
You just have to provide a property for dropping the current text to be shown for the binding to work, and it will adjust it for you.

Sharing the same FrameworkElement and DrawingVisuals instance across various Grids (WPF .net 4 )

I am doing an application where I draw using PathFigure, LineSegment, ... stored as DrawingVisuals in a FrameworkElement.
I need to show the same drawing in 2 different UI elements (Grids or Panels, whatever...)
One will be used by the user to draw and the second will just allow to visualize the same drawing, zoom and scroll without affecting the 1st UI element viewport.
I will have more than 2000 DrawingVisuals, it would be stupid to duplicate them...
I am currently scratching my head to figure out the best way to do it.
What is, in your opinion, the right solution to achieve this?
More information:
I have tried the obvious way, with a simple XAML
<Grid>
<StackPanel>
<Border Name="B1" Background="Bisque" Width="400" Height="200"/>
<Border Name="B2" Background="Beige" Width="400" Height="200"/>
</StackPanel>
</Grid>
Then a simple code
var map = new VdMap();
B1.Child = map;
var elem = new ElementVisual(map);
elem.StartElement(20, 20);
elem.AddSegment(80, 60);
elem.AddSegment(10, 80);
elem.EndElement();
elem.Draw();
B2.Child = map;
VdMap is a FrameworkElement
ElementVisual, StartElement, AddElement are my internal functions. The important thing are:
B1.Child = map; //I attach my Map to the border
B2.Child = map; //I try to attach the same FrameworkElement to the second border.
And I get the run time error "Specified element is already the logical child of another element. Disconnect it first."
Well looks like it is going to be harder than I thought.
Ok,
I probably found the right way to do it: The VisualBrush and a Rectangle.
var mapClone = new VisualBrush { Visual = map };
rectangle = new Rectangle {
Width = 300, Height = 300,
Stroke = Brushes.Black, HorizontalAlignment = HorizontalAlignment.Left,
Fill = mapClone };
B2.Children.Add(rectangle);
I have now in B2 a visual copy of my map that I can scale and transform separately.
Any better idea is welcomed.

need to add textBlocks to a group box and clear when data is loaded in a graph control

I'm creating a graph control. what i'm doing for adding x and y axis tally labels is i'm
adding a text block to each tally mark and show the value related to that tally mark.
but when i need to load data form the database and redraw the textbolcks again and refresh the graph area i can't remove the older textblocks they are still on the graph pane.
to overcome this problem i thought to put the text blocks in side a group box and when graph pane is redrawn to delete the group box elements and put them again..
is this approach correct?
please tell me how to put elements to groupbox in code behind class?
and please tell me if their is any other solution to my problem.
regards,
rangana.
In WPF there are many solutions to most problems. I will discuss three possible solutions to your problem - the one you describe and two others. You can decide which will work best for you.
Solution 1: Using TextBlock objects to disply the labels
It sounds like you have a Canvas and you're adding a TextBlock to it for each tick mark. This is a viable solution if performance isn't too critical and you can't use data binding.
There are two ways to remove the TextBlocks in this case:
You can keep a List<TextBlock> containing all the Textblocks list of the TextBlocks you created the last time you created the labels. Whenever you recreate the labels, run through this list and remove each TextBlock on the list from the containing panel (the Canvas)
You can create a new Canvas and put the TextBlocks on it, then delete the whole Canvas when you relabel.
Here is an example of the second technique, since it is slightly more efficient:
class MyGraphBuilder
{
Canvas _labelCanvas;
...
void AddLabels()
{
// Remove old label canvas, if any
if(_labelCanvas!=null)
_graphCanvas.Children.Remove(_labelCanvas);
// Add new label canvas
_labelCanvas = new Canvas();
_graphCanvas.Children.Add(_labelCanvas);
// Create labels
foreach(...)
{
...
_labelCanvas.Add(new TextBlock ...
}
...
}
}
Solution 2: Using data binding
In WPF you can create many graphs without writing a single line of code! WPF's built in data binding is sufficient to create relatively complex bar charts, etc.
Here is an example of using data binding to create a simple bar chart:
<ItemsControl ItemsSource="{Binding myData}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<DockPanel>
<TextBlock Width="50" Text="{Binding Label}"/>
<Rectangle VerticalAlignment="{Stretch}" Width="{Binding Value}">
<Rectangle.LayoutTransform>
<ScaleTransform ScaleX="10" /> <!-- Scale factor here, can be binding too -->
</Rectangle.LayoutTransform>
</Rectangle>
<TextBlock Text="{Binding Value}" FontSize="8"/>
</DockPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Numeric labels can be added to the horizontal axis by using a second ItemsControl laid out horizontally, and with its data template a fixed width and showing numbers and tick marks.
Solution 3: Using low level Drawing classes
Build your graph by constructing a DrawingGroup object and adding GeometryDrawing and GlyphRunDrawing objects to it, then putting the DrawingGroup inside DrawingVisual and add that to your main Panel.
You should use one GeometryDrawing or GlyphRunDrawing for each set of items sharing a given brush and pen. For example if your axes and tick marks are all the same color and width, create a single GeometryDrawing for all of them, but if each tick mark is a differnet color, create multiple GeometryDrawing objects.
You will create a Geometry object for each GeometryDrawing. For the best efficiency it should be a StreamGeometry, but the other Geometry classes also work well, may be easier to use, and may be initialized in XAML. Creating a PathGeometry or EllipseGeometry is probably already familar to you so I'll focus on creating a StreamGeometry. You do this by calling the Open method in a using() statement, then writing to the returned context. Here is an example:
Geometry BuildAxesAndTicksGeometry()
{
// First create geometry
var geometry = new StreamGeometry();
using(var context = geometry.Open())
{
// Horizontal axis
context.BeginFigure(new Point(0,0), false, false);
context.LineTo(new Point(_width, 0), true, false);
// Vertical axis
context.BeginFigure(new Point(0,0), false, false);
context.LineTo(new Point(0, _height), true, false);
// Horizontal ticks
for(int i=0; i<_nTicksHorizontal; i++)
{
context.BeginFiture(new Point(i * _tickSpacing, -10), false, false);
context.LineTo(new Point(i * _tickSpacing, 10), true, false);
}
// Do same for vertical ticks
}
// Now add it to a drawing
return new GeometryDrawing { Geometry = geometry, Stroke = _axisPen };
}
Drawing BuildDrawing()
{
var mainDrawing = new DrawingGroup();
mainDrawing.Add(BuildAxesAndTicksGeometry());
... // Add other drawings, including one or more for the data
return mainDrawing;
}
void UpdateDrawing()
{
myDrawingVisual.Drawing = BuildDrawing(); // where myDrawingVisual is defined in the XAML
}
Comparison of solutions
For most cases I would recommend solution 2 or 3, for these reasons:
If the graph is simple enough to use data binding it will save you a lot of time. Go with solution 2.
If the graph cannot be done with data binding, using Drawing objects is approximately as simple as any other technique, and can perform better. Go with solution 3.
In your case if you've already invested significant work into your Solution 1, you may want to stick with it even though it probably isn't the best.

New to WPF - What Control to Use / Getting Started?

I'm a WPF n0ob and I'm struggling with selecting the appropriate control to get the layout I want.
What I'm trying to do is draw a bunch of squares (virtual post-it notes) onto the screen. Each note is going to be a decent size (~150 pixels or so) and there could be hundreds of these notes. I want the whole thing to be scrollable so that you can resize the window however you like and the whole thing should be zoomable.
I've done this and it works.
But what I've done seems awfully wrong....
In the code, I'm dynamically creating post it notes and adding them to a giant canvas. I'm manually doing the math to determine where to place each note and how big the canvas should be. I added some labels at the top and had to go back and add a 'Y Offset' value to push all the squares down. I actually generate three different canvas controls and then add each one of them to a stack panel that is inside of a ScrollViewer. I added a scroll bar and set the the stack panel to zoom in and out as you adjust the bar.
It 'works', but I feel like I'm really not using WPF the way it's meant to be used. I tried achieving the same thing with a grid, but the grid didn't seem to want to size itself appropriately.
Can someone tell me a 'better' way to achieve the same look?
Here's my Xaml code - as you can see; there isn't much to it....
<Window x:Class="Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition Height="25" />
<RowDefinition />
</Grid.RowDefinitions>
<Slider x:Name="ZoomSlider" Minimum="0.01" Value="1" Maximum="2" Margin="0,0,0,6" />
<ScrollViewer x:Name="MyScroller" Grid.Row="1" HorizontalScrollBarVisibility="Visible" HorizontalContentAlignment="Center" VerticalContentAlignment="Center">
<StackPanel x:Name="TicketsGrid" Background="White" HorizontalAlignment="Center">
</StackPanel>
</ScrollViewer>
</Grid>
And then here is what I'm doing in code (ugly!!!)
For Each myWorkItem As WorkItem In myWorkItems
Dim newRect As New Border
newRect.Width = TicketSizeX
newRect.Height = TicketSizeY
If myWorkItem.State.ToUpper.Contains("HOLD") Then
newRect.Background = New SolidColorBrush(Colors.Purple)
Else
newRect.Background = New SolidColorBrush(Color)
End If
newRect.CornerRadius = New System.Windows.CornerRadius(5)
newRect.BorderThickness = New System.Windows.Thickness(1)
newRect.BorderBrush = New SolidColorBrush(Colors.Black)
Dim myPanel As New StackPanel
newRect.Child = myPanel
Dim lblTitle As New Label
lblTitle.Content = myWorkItem.Id
lblTitle.FontWeight = System.Windows.FontWeights.Bold
Dim lblDesc As New TextBlock
lblDesc.Text = myWorkItem.Title
lblDesc.TextWrapping = TextWrapping.Wrap
myPanel.Children.Add(lblTitle)
myPanel.Children.Add(lblDesc)
newRect.SetValue(Canvas.LeftProperty, CType(((TicketCount Mod TicketsXPerUser) * TicketStepX) + (xOffset * TicketStepX * TicketsXPerUser), Double))
newRect.SetValue(Canvas.TopProperty, CType(((Math.Floor((TicketCount / TicketsXPerUser)) * TicketStepY)) + NameLabelHeight, Double))
myCanvas.Children.Add(newRect)
TicketCount += 1
Next
MyCanvas.Width = (TicketStepX * TicketsXPerUser) * myTFS.SharedCodeTeam.Count
MyCanvas.Height = (CType(((Math.Floor((MaxTicket / TicketsXPerUser)) + 1) * TicketStepY), Double))
TicketsGrid.Children.Add(MyCanvas)
ScrollViewer with an ItemsControl inside.
Bind the ItemsSource property of the ItemsControl to an ObservableCollection<PostIt> (where PostIt is a plain old CLR object with all the info that goes on the post it).
Add a DataTemplate to the ItemsTemplate property of the ItemsControl
Add controls to the DataTemplate and bind them directly to an instance of PostIt
Add PostIt instances to the ObservableCollection<PostIt> in your code.
The ScrollViewer handles all scrolling. That's all you need.
The ItemsControl is designed to bind against a collection. For each instance in the collection, it figures out what DataTemplate to use, creates a copy of the template, sets the root's DataContext to the instance it pulled from the collection, and adds the template to itself. It does this for each instance found in the collection.
In your codebehind, all you need to do is create a bunch of PostIts and add them to the collection. No godawful construction of UI elements like you're doing. Urgh.
If you can grasp this concept, you are a step away from understanding MVVM, the Model-View-Controller pattern in WPF. Read about it, try it out. Its a very simple way of making very complex applications with complex UI but with a minimum of code (and none of that crap you're doing currently).

Silverlight: Cannot manage to get ScrollViewer and WrapPanel to work together

I'm using a WrapPanel to format some text. During runtime I add TextBlocks and StackPanels as Children. Obviously I need a scrollbar depending on the data size. Searching the web I found multiple answers which all propose to put a ScrollViewer around the WrapPanel. That makes sense to me, but I cannot get it to work. Here's my code:
scrollView = new ScrollViewer();
scrollView.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled;
scrollView.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
scrollView.HorizontalAlignment = HorizontalAlignment.Stretch;
scrollView.VerticalAlignment = VerticalAlignment.Stretch;
scrollView.Margin = new Thickness(0);
scrollView.BorderThickness = new Thickness(0);
textPanel = new WrapPanel();
textPanel.Width = Width;
scrollView.Content = textPanel;
That does not work. The ScrollViewer seems to grow with the contained WrapPanel. If I set the vertical scroll bar to visible, I can see that the scrollbar grows with the content. But the ScrollViewer grows out of the containing window and therefor no scrollbar is displayed ever.
Any hint what I might be doing wrong?
cheers,
Achim
You need something to contrain the size of the WrapPanel and the ScrollViewer - you could set the width and height of the viewer then use an element binding on the wrappanel:
<ScrollViewer x:Name="ScrollViewer1"
Width="200"
Height="200"
ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.HorizontalScrollBarVisibility="Visible" >
<controls:WrapPanel
Width="{Binding ElementName=ScrollViewer1, Path=Width}"
Height="{Binding ElementName=ScrollViewer1, Path=Height}">
Hope that helps.
Ian

Resources