WPF: Placing a textbox randomly on a canvas - wpf

I have a canvas of sorts derived from a 'Panel' used for custom drawing of lines and other geometries, all from VB code. I got this approach from a book and I'm not sure it's the best approach. The drawing part works so far for me.
But what I need is to put a textbox control on the control containing text which can be edited by the user. The textbox needs to be placed at coordinates determined dynamically and later deleted. There will probably be other controls handled so.
The following code does nothing:
tb = New TextBox()
tb.Text = "How now brown cow?"
tb.BorderThickness = New Thickness(3)
tb.BorderBrush = Brushes.CadetBlue
drawingSurface.Children.Add(tb)
This is the definition of my DrawingCanvas:
Public Class DrawingCanvas
Inherits Panel
Private visuals As New List(Of Visual)()
Private hits As New List(Of DrawingVisual)()
Protected Overrides Function GetVisualChild(ByVal index As Integer) As Visual
Return visuals(index)
End Function
Protected Overrides ReadOnly Property VisualChildrenCount() As Integer
Get
Return visuals.Count
End Get
End Property
Public Sub AddVisual(ByVal visual As Visual)
visuals.Add(visual)
MyBase.AddVisualChild(visual)
MyBase.AddLogicalChild(visual)
End Sub
Public Sub DeleteVisual(ByVal visual As Visual)
visuals.Remove(visual)
MyBase.RemoveVisualChild(visual)
MyBase.RemoveLogicalChild(visual)
End Sub
Public Function GetVisual(ByVal point As Point) As DrawingVisual
Dim hitResult As HitTestResult = VisualTreeHelper.HitTest(Me, point)
Return TryCast(hitResult.VisualHit, DrawingVisual)
End Function
Public Function GetVisuals(ByVal region As Geometry) As List(Of DrawingVisual)
hits.Clear()
Dim parameters As New GeometryHitTestParameters(region)
Dim callback As New HitTestResultCallback(AddressOf Me.HitTestCallback)
VisualTreeHelper.HitTest(Me, Nothing, callback, parameters)
Return hits
End Function
Private Function HitTestCallback(ByVal result As HitTestResult) As HitTestResultBehavior
Dim geometryResult As GeometryHitTestResult = CType(result, GeometryHitTestResult)
Dim visual As DrawingVisual = TryCast(result.VisualHit, DrawingVisual)
If visual IsNot Nothing AndAlso geometryResult.IntersectionDetail = IntersectionDetail.FullyInside Then
hits.Add(visual)
MsgBox("Ouch")
End If
Return HitTestResultBehavior.Continue
End Function
End Class
Here is the XAML. I added a textbox to the DrawingCanvas just to see if something appears. Nothing did. In fact, I want to do this in code, not XAML. I thought I could hide or move it around dynamically.
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Music"
Title="MainWindow" Height="539" Width="892">
<DockPanel>
<Menu DockPanel.Dock="Top" Name="MainMenu" VerticalAlignment="Top" Height="25">
<MenuItem Name="File" Header="File">
<MenuItem Name="Open" Header="Bla bla..."/>
</MenuItem>
</Menu>
<local:DrawingCanvas DockPanel.Dock="Bottom" x:Name="drawingSurface" RenderTransformOrigin="0.5,0.5" >
<TextBox Height="0" Name="TextBox1" Width="45" Text="How now brown cow?" />
</local:DrawingCanvas>
</DockPanel>
</Window>
Thanks for helping a nooby. A solution would be very useful for me. This was easy with windows forms, but I need the drawing speed of WPF.

I think you are a bit off here. In WPF you have a control called Canvas. I would suggest you use that instead of your own "DrawingCanvas", which I can't get to work btw. :( (For some reason I cant create code blocks so if someone can edit it I would be pleased)
Anyhow,
<local:DrawingCanvas DockPanel.Dock="Bottom" x:Name="drawingSurface" RenderTransformOrigin="0.5,0.5" >
<TextBox Height="0" Name="TextBox1" Width="45" Text="How now brown cow?" />
</local:DrawingCanvas>
Turns into:
<Canvas x:Name="drawingSurface">
</Canvas>
And then to add a textbox just do as your current code:
Dim tb as New TextBox
drawingSurface.Children.Add(tb)
This should give you what you need.
Heres the code for adding a rectangle to your canvas.
Private Sub DrawBackground()
Dim Rect As New Rectangle()
Rect.Height = 50
Rect.Width = 50
Rect.Fill = Brushes.Cornsilk
drawingSurface.SetTop(Rect, 30)
drawingSurface.SetLeft(Rect, 100)
drawingSurface.Children.Add(Rect)
End Sub

I'll add another answer that might be more in the line of what you are looking for. This is a class that inherits from Canvas that will allow you to draw stuff in the same way as say you do in your comment.
I also creates a textbox on a random location when it is created.
Public Class DrawingCanvas
Inherits Canvas
Public RandomTextBox As New TextBox
Protected Overrides Sub OnRender(dc As System.Windows.Media.DrawingContext)
Dim brush As Brush = Brushes.Black
Dim drawingPen As Pen = New Pen(Brushes.Green, 3)
dc.DrawRectangle(brush, drawingPen, New Rect(5, 5, Me.ActualWidth - 5, Me.ActualHeight - 5))
RandomTextBox.Text = "Herpdiderp"
If Not Me.Children.Contains(RandomTextBox) Then
Dim r As New Random()
RandomTextBox.Height = 23
RandomTextBox.Width = 100
Me.SetTop(RandomTextBox, r.Next(0, Me.ActualHeight - RandomTextBox.Height))
Me.SetLeft(RandomTextBox, r.Next(0, Me.ActualWidth - RandomTextBox.Width))
Me.Children.Add(RandomTextBox)
End If
End Sub
End Class

This is not a full answer. #WozzeC, you were right about using the canvas - almost.
I have managed to solve this in xaml alone - I want to eventually solve it in vb.net.
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DockPanel HorizontalAlignment="Stretch" Name="DockPanel1" VerticalAlignment="Stretch" >
<Menu Height="23" DockPanel.Dock="Top" Name="Menu1" VerticalAlignment="Top" />
<Canvas Name="Canvas1" Background="Aquamarine">
<TextBox Canvas.Left="118" HorizontalScrollBarVisibility="Disabled" Canvas.Top="81" AcceptsReturn="True" Height="auto" Name="TextBox1" Width="68" Text="Herpdiderp" BorderThickness="0" Background="Aquamarine" />
</Canvas>
</DockPanel>
</Grid>
And here is a piece of code that expands the text as needed. I think it's almost totally cool. It expands both to the right and downwards, as if you're actually typing on the form. It adds a little too much on the right, but it's not visible in this version because the background color is the same.
Here is the event code that expands it to the right.
Imports System.Globalization
Class MainWindow
Private Sub TextBox1_TextChanged(sender As System.Object, e As System.Windows.Controls.TextChangedEventArgs) Handles TextBox1.TextChanged
Dim ft As New FormattedText(TextBox1.Text, CultureInfo.GetCultureInfo("en-us"), FlowDirection.LeftToRight, New Typeface("Verdana"), 16, Brushes.Black)
TextBox1.Width = ft.Width
End Sub
End Class
I tried this with my existing solution and the textbox does not appear. I made the DrawingCanvas into a plain Canvas and commented out all the code referring to the DrawingCanvas. And the textbox does appear. The problem is this: I need the functionality in the DrawingCanvas - which derives from Canvas. But because the baseclass methods are Protected, I can't get to them. I can only use them in a derived class, unless there is another way I don't know about.
Any ideas about how to solve this?

Related

Odd Scrollbar behaviour in WPF control

I have made the following simple control, which I add to another window as a popup (starts hidden, gets changed to visible on a button click)
<UserControl x:Class="AddGroupsCtrl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:ASManager2017"
mc:Ignorable="d" d:DesignWidth="255" Height="413">
<GroupBox x:Name="groupBox" Header=" Active Directory Groups" HorizontalAlignment="Stretch" Margin="0,0,0,0" VerticalAlignment="Stretch" Height="350" Width="233" BorderBrush="{DynamicResource {x:Static SystemColors.ActiveBorderBrushKey}}">
<Grid Height="auto">
<ListBox x:Name="grouplistBox" HorizontalAlignment="Stretch" Height="auto" Margin="0,0,0,0" VerticalAlignment="Stretch" Width="auto" SelectionMode="Extended" ScrollViewer.VerticalScrollBarVisibility="Visible" Background="{DynamicResource fadeBrush}"/>
<Button x:Name="addButton" Content="Add" HorizontalAlignment="Right" Margin="0,0,22,0" VerticalAlignment="Bottom" Width="75"/>
<Button x:Name="button" Content="X" HorizontalAlignment="Right" Margin="0,0,22,0" VerticalAlignment="Top" Width="20" Height="20" Background="{DynamicResource {x:Static SystemColors.ControlLightLightBrushKey}}"/>
</Grid>
</GroupBox>
In the code behind this I have the following fragment....
For Each grp As String In gsList
grouplistBox.Items.Add(grp)
Next
Everything seems to work fine, apart from the scroll bar at the side of the listbox. This starts at about half the height of the entire control, and when trying to slide it down the list doesn't scroll, but the scroller shrinks in size until it gets to a small bar, and then starts scrolling to contents of the window. Likewise dragging the bar back up: the listbox will get to the top of the list when the bar is about halfway up the scroll-area, and then the bar will expand.
Can anyone help me correct this behaviour/explain what I am doing wrong?
Thanks.
Pete.
After more experimenting, things are getting weirder....
Imports AshbyTools
Imports System.DirectoryServices.AccountManagement
Imports System.ComponentModel
Public Class AddGroupsCtrl
Dim _domainString As String
Dim _ouString As String
Public Event addClicked(ByVal glist As List(Of String))
<Description("ouString"), DisplayName("OU String"), Category("Data")>
Public Property ouString As String
Get
Return _ouString
End Get
Set(value As String)
_ouString = value
End Set
End Property
<Description("domainString"), DisplayName("Domain String"), Category("Data")>
Public Property domainString As String
Get
Return _domainString
End Get
Set(value As String)
_domainString = value
End Set
End Property
Private Sub button_Click(sender As Object, e As RoutedEventArgs) Handles button.Click
Me.Visibility = Visibility.Hidden
End Sub
Public Sub loadGroups()
grouplistBox.Items.Clear()
Dim groupCTX As PrincipalContext = ADTools.getConnection(domainString, ouString)
Dim gList As List(Of GroupPrincipal) = ADTools.getManagedGroups(groupCTX)
Dim gsList As New List(Of String)
For Each grp As GroupPrincipal In gList
If Not (grp.DistinguishedName.Contains("Staff Groups") Or grp.DistinguishedName.Contains("Subject Groups") Or grp.DistinguishedName.Contains("Tutor Groups")) Then
gsList.Add(grp.DisplayName)
End If
Next
'For n As Integer = 1 To 70
' grouplistBox.Items.Add("item" & n)
'Next
gsList.Sort()
For Each grp As String In gsList
grouplistBox.Items.Add(grp)
Next
End Sub
Private Sub addButton_Click(sender As Object, e As RoutedEventArgs) Handles addButton.Click
Dim ret As New List(Of String)
For Each grp In grouplistBox.SelectedItems
ret.Add(grp.ToString)
Next
RaiseEvent addClicked(ret)
End Sub
End Class
Upon loadGroups() if I replace the
For Each grp As String In gsList
grouplistBox.Items.Add(grp)
Next
with the commented out code, the scrollbar works perfectly.
With the list of strings from gsList, I get the odd behaviour.
I could post the entire project, but since it is hard-coded for our active directory structure it wouldn't compile on another system. (also, as I am just using this to learn WPF and it's an internal tool, I've hard-coded numerous passwords)
Ok, I have solved the issue. And the answer is even more confusing. Turns out that in the code, where I got the DisplayName of the group to add to the list of string, it was returning 'Nothing' for each of them. Yet somehow displayed the list correctly (although with a screwy scrollbar). Now I have changed it to return grp.name rather than displayname, and EVERYTHING works as I'd expect.

PopulatingTextBoxes with Items from ListBox

I have a ListBox1 and a series of TextBoxes from TextBox9 onwards.I can populate the ListBox1 with Items(in sequence).I am trying to transfer those items to TextBoxes in the same order(one item for one TextBox).
I tried this code:
Dim x As Integer = ListBox1.SelectedIndex
For x = 0 To 50
For Each ListBoxItem In ListBox1.Items
ListBox1.SelectedItem = "TextBox" & (x+9) & ".Text"
Next
Next x
The code compiles but nothing happens when i click the button.Can anyone help me out?
Thanks in advance
Venkatraman
One way among others could be using the FrameworkElement.FindName() method, see also the following sample.
FWIW, in WPF using the MVVM pattern makes much more sense than one might expect at first glance, where usually a ViewModel would provide UI content among other things, and you usually would not want to do too much in the View's code-behind.
Sample.XAML:
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" SizeToContent="WidthAndHeight"
Loaded="Window_Loaded">
<StackPanel x:Name="stackPanel">
<ListBox x:Name="l1"/>
<TextBox x:Name="t1"/>
<TextBox x:Name="t2"/>
<TextBox x:Name="t3"/>
<Button Content="Test" Height="25" Click="Button_Click"/>
</StackPanel>
</Window>
Sample.VB:
Class MainWindow
Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs)
l1.Items.Add("Item1")
l1.Items.Add("Item2")
l1.Items.Add("Item3")
l1.Items.Add("Item4")
End Sub
Private Sub Button_Click(sender As Object, e As RoutedEventArgs)
Dim i As Integer
Dim s As String
For Each item As String In l1.Items
i = i + 1
s = String.Format("t{0}", i)
Dim control = Me.FindName(s)
If TypeOf control Is TextBox Then
control.Text = item
End If
Next
End Sub
End Class

Trying to bind a list of objects to a WPF datagrid control and seeing blank lines

I am stuck and need some help. I have spent a full day trying to sort this one out. I have read a lot of examples of how this should be done and it appears that I have done it correctly but clearly I have missed something. I have a blank form with one button and one datagrid. When I click the button I want to load a list of points into the datagrid control. The data must be getting loaded because I see three blank lines in the grid (no headers), but no data. Help please! Thanks!
Class MainWindow
Class Point
Public Inc As String
Public AZ As String
Public MD As String
Public TD As String
End Class
Private Sub Button_Click(sender As Object, e As RoutedEventArgs)
Dim mySurvey As New List(Of Point)
Dim myPoint1 As New Point
Dim myPoint2 As New Point
myPoint1.AZ = "1"
myPoint1.Inc = "2"
myPoint1.MD = "100"
myPoint1.TD = "98"
myPoint2.AZ = "10"
myPoint2.Inc = "20"
myPoint2.MD = "1000"
Point2.TD = "980"
mySurvey.Add(myPoint1)
mySurvey.Add(myPoint2)
dgSurvey.ItemsSource = mySurvey
End Sub
End Class
XAML
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Button Content="Button" HorizontalAlignment="Left" Height="28" Margin="45,30,0,0" VerticalAlignment="Top" Width="57" Click="Button_Click"/>
<DataGrid x:Name="dgSurvey" HorizontalAlignment="Left" Margin="45,78,0,0" VerticalAlignment="Top" Height="172" Width="275"/>
</Grid>
For binding to work you should define properties in your Point class, not public fields.
This is explicitly stated in MSDN: Binding Sources Overview:
The properties you use as binding source properties for a binding must be public
properties of your class. Explicitly defined interface properties cannot be
accessed for binding purposes, nor can protected, private, internal,
or virtual properties that have no base implementation.
You cannot bind to public fields.

How do you create an event to undo the previous event in WPF with VB.NET

I was wondering if there is any relatively simple way to essentially create an 'undo' button that would undo whatever event happened right before it. The problem is, I can't do it explicitly (for example, if the background was white, and then it became red, I can't just have the undo button reset the background as white). I can't do it this way because I won't know which event happened last, there could be a number of events that could have happened, and I don't want an individual undo button for every single event.
To provide an example, I have a few labels in a grid, and when I mouse over any label it changes to a bigger size, and all the other labels become a standard (smaller) size. However, sometimes one of the labels will already be a bigger size (from a button, or such) -- lets call this label1. So when I mouse over a different label -- lets call this label2 -- then label2 becomes bigger whereas label1 is small now. But when I move the mouse off of label2, I want label1 to be big again whereas label2 should be small again. Thanks for any help/ strategies in advance!!
P.S. I'm pretty new to WPF so the simpler the solution the better :) But anything is appreciated!
EDIT: I think an easy way of asking this is: Is there any way to create a MouseLeave event that undoes whatever was done by a MouseEnter event?
I don't do VB so sorry for conventions, but this should work for you as a quick example:
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<StackPanel x:Name="stackPanel">
<Label FontSize="10" Margin="10" MouseEnter="OnMouseEnter" >1</Label>
<Label FontSize="10" Margin="10" MouseEnter="OnMouseEnter" >2</Label>
<Label FontSize="10" Margin="10" MouseEnter="OnMouseEnter" >3</Label>
<Label FontSize="10" Margin="10" MouseEnter="OnMouseEnter" >4</Label>
</StackPanel>
</Window>
code behind:
Class MainWindow
Dim _mouseLeaveSize As Double = 10
Dim _mouseEnterSize As Double = 20
Private Sub OnMouseEnter(ByVal sender As Object, ByVal e As MouseEventArgs)
For Each child As Visual In stackPanel.Children
SetLabelLeaveProperties(child)
Next
Dim label = CType(sender, Label)
label.FontSize = _mouseEnterSize
End Sub
Private Sub SetLabelLeaveProperties(ByVal myVisual As Visual)
Dim label = TryCast(myVisual, Label)
If label Is Nothing Then
'iterate thru children to see if anymore labels
For i As Integer = 0 To VisualTreeHelper.GetChildrenCount(myVisual) - 1
Dim child = VisualTreeHelper.GetChild(myVisual, i)
Dim l = TryCast(child, Label)
If l Is Nothing Then
SetLabelLeaveProperties(child) 'Enumerate children of the child visual object.
Else
l.FontSize = _mouseLeaveSize
End If
Next i
Else
label.FontSize = _mouseLeaveSize
End If
End Sub

Showing/Closing forms in wpf

Im developing an application using wpf template I have these 2 windows:
MainWindow.xaml and
JungleTimer.vb which is a Windows Form
I have a button in my main windows which shows JungleTimer form using this code:
Dim JungleTimer As New JungleTimer
JungleTimer.Show()
But as you see, clicking this button multiple times will show multiple JungleTime form.
I tried to use this code to check if JungleTimer is visible but it doesn't work:
Dim JungleTimer As New JungleTimer
If JungleTimer.Visible = False Then
JungleTimer.Show()
End If
I also need the code to close the JungleTimer form.
As you are creating a new JungleTimer each time you click the button you will always get a new instance of the window. What you need to do is declare a field within the class of the type JungleTimer. Initially this will be null (Nothing). When you click the button, check if this field has a value or is still null. If still null, set it to a new JungleTimer and show it. If it isn't null, activate the existing window without creating a new instance. you'll also need to detect when the window closes so that you can set the field back to null.
For a demo, create a new WPF application with two windows, MainWindow (the main window) and JungleTimer.
XAML for MainWindow:
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<StackPanel VerticalAlignment="Center">
<Button Width="100" Height="30" Click="Jungle_Click">Jungle Me</Button>
<Button Width="100" Height="30" Click="DeJungle_Click">De-Jungle Me</Button>
</StackPanel>
VB for MainWindow (sorry if it's clumsy, I haven't done VB for ten years or so):
Class MainWindow
Private WithEvents _jungleTimer As JungleTimer
Private Sub Jungle_Click(sender As Object, e As RoutedEventArgs)
If _jungleTimer Is Nothing Then
_jungleTimer = New JungleTimer
_jungleTimer.Show()
Else
_jungleTimer.Activate()
End If
End Sub
Private Sub DeJungle_Click(sender As Object, e As RoutedEventArgs)
If Not _jungleTimer Is Nothing Then
_jungleTimer.Hide()
_jungleTimer = Nothing
End If
End Sub
Private Sub CloseHandler() Handles _jungleTimer.Closed
_jungleTimer = Nothing
End Sub
End Class
XAML for JungleWindow:
<Window x:Class="JungleTimer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="JungleTimer" Height="300" Width="300">
<Grid>
<Label HorizontalAlignment="Center" VerticalAlignment="Center">
Jungle!
</Label>
</Grid>

Resources