Track WPF Grid Column Width Upon Resizing - wpf

I am using Grid splitter to resize columns in wpf application. I want some event that occurs when I am done with resizing. Grid Splitter has no events in it.
So I try using events from grid columns
void col_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_dragStart = true;
_currentColWidth = (sender as ColumnDefinition).ActualWidth;
}
bool _dragStart;
double _currentColWidth;
void col_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
_dragStart = false;
if ((sender as ColumnDefinition).ActualWidth != _currentColWidth)
{
}
}
This is the screenshot of the application and these are the two columns with and without resizing

What do you mean with GridSplitter doesn't have any events ?
XAML
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="Column1" Width="Auto"/>
<ColumnDefinition x:Name="Column2" Width="*"/>
</Grid.ColumnDefinitions>
<GridSplitter x:Name="GridSplitter1" VerticalAlignment="Stretch" DragCompleted="GridSplitter1_DragCompleted" HorizontalAlignment="Right" Grid.Column="0" Background="Black" BorderBrush="Black" BorderThickness="2" Width="2"/>
CODE:
void GridSplitter1_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)
{
void GridSplitter1_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)
{
MessageBox.Show("Column1 : " + Column1.Width + "\n" + "Column2: " + Column2.Width);
}
}
Works very fine for me

Bind the gridsplitter position (or some width in the column) against a property. In the Binding use the delay option (new in .NET 4.5)
This was introduced for Slider-Controls, etc. which update much too often. Set the delay to a few hundred milliseconds, then you only get the final updated value.
See more here: http://www.jonathanantoine.com/2011/09/21/wpf-4-5-part-4-the-new-bindings-delay-property/

Related

SizeToContent with a Listbox

I'm using SizeToContent="WidthAndHeight" so that I can size a window to the size of the currently displayed controls. This works fine, however, I'm running into an issue when displaying and adding items to a listbox. When adding a great number of items to the listbox this will cause the window to extend below the edge of the monitor. I notice that without the SizeToContent="WidthAndHeight" I can easily get the Listbox to limit itself and use a scrollbar. However, this has the negative of displaying all the extra unused space that the listbox with take up before I have made it visible. I have added a small sample solution below to display this behaviour.
NOTE window does not need to be resizable. It just needs to be efficient on space and I would rather not use absolute values if possible.
Explaination of desired behaviour; user only sees the button, window has no unused space around the button. When the button is clicked the Listbox is now visible but has not extended beyond the screen (i.e. a scroll bar has been added) and the button beneath is visible.
...ResizeMode="CanMinimize" SizeToContent="WidthAndHeight">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*"/>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Button Content="hello" Click="Button_Click"/>
<ListView Name="lbx" Grid.Row="1" Visibility="Collapsed">
</ListView>
<Button Name="btn" Content="hello" Grid.Row="2" Visibility="Collapsed"/>
</Grid>
private void Button_Click(object sender, RoutedEventArgs e)
{
btn.Visibility = Visibility.Visible;
lbx.Visibility = Visibility.Visible;
//Have tried various combinations of settings in code at runtime
this.SizeToContent = SizeToContent.Width;
this.Height = Double.NaN;
for (int i = 0; i < 1000; i++)
{
lbx.Items.Add("Num:" + i);
}
}
I've also tried this with a dockpanel instead of a Grid;
<Button Content="hello" Click="Button_Click" DockPanel.Dock="Top"/>
<Button Name="btn" Content="hello" DockPanel.Dock="Bottom" Visibility="Collapsed"/>
<ListView Name="lbx" Visibility="Collapsed">
</ListView>
Try swapping SizeToContent="WidthAndHeight" with SizeToContent="Width", the window starts up showing all the empty space that will later be used by the collapsed controls.
You could set the MaxHeight of the window based on the actual height of the ListBox and the screen, e.g.:
private void Button_Click(object sender, RoutedEventArgs e)
{
btn.Visibility = Visibility.Visible;
lbx.Visibility = Visibility.Visible;
for (int i = 0; i < 5; i++)
{
lbx.Items.Add("Num:" + i);
}
Dispatcher.Invoke(() =>
{
MaxHeight = Math.Min(SystemParameters.WorkArea.Height - Top, grid.ActualHeight);
}, System.Windows.Threading.DispatcherPriority.Background);
}
This seems like a good option. One drawback is that with a small number of items it still introduces a scrollbar even when the listbox items should be able to fit.
Introduce an offset then:
private void Button_Click(object sender, RoutedEventArgs e)
{
btn.Visibility = Visibility.Visible;
lbx.Visibility = Visibility.Visible;
for (int i = 0; i < 5; i++)
{
lbx.Items.Add("Num:" + i);
}
grid.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
grid.Arrange(new Rect(new Size(grid.DesiredSize.Width, grid.DesiredSize.Height)));
Height = Math.Min(SystemParameters.WorkArea.Height - Top, grid.ActualHeight + 40);
}

WPF - Hidden Window Shows In Screenshot

My simple goal is to take a screenshot.
My application has a main window that has multiple purposes and one of them is taking a screenshot.
What I do is hide the window by using MainWindow.Hide(); and then showing a second window that handles the screenshot taking process.
this.Hide();
SnapshotManager snapshotMgr = new SnapshotManager();
snapshotMgr.Closed += SnapshotMgrClosed;
snapshotMgr.Show();
The second window is basically this:
<Grid>
<Image Name="MainImage" Stretch="None" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
</Image>
<Border BorderBrush="Transparent" BorderThickness="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Border.Background>
<SolidColorBrush Color="Black" Opacity="0.4"/>
</Border.Background>
</Border>
</Grid>
with the properties:
AllowsTransparency="True"
Background="Transparent"
WindowStyle="None"
When the user clicks a button to take the screenshot I hide the main window and show this one.
Then I save the screen to the image in my second window like so:
Bitmap snapshot = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height);
Graphics graphics = Graphics.FromImage(snapshot);
graphics.CopyFromScreen(new System.Drawing.Point(0,0), new System.Drawing.Point(0,0), Screen.PrimaryScreen.Bounds.Size, CopyPixelOperation.SourceCopy);
//-----------------------------------
// Save the screen to MainImage
//-----------------------------------
MemoryStream ms = new MemoryStream();
snapshot.Save(ms, ImageFormat.Bmp);
ms.Position = 0;
BitmapImage bi = new BitmapImage();
bi.BeginInit();
bi.StreamSource = ms;
bi.EndInit();
MainImage.Source = bi;
The problem is that for some reason my main window shows up in the screenshot.
The MainWindow.Hide() probably doesn't work like I think it does.
How can I take a screenshot with the main window?
Thank you!
Edit:
The following code works but uses Sleep(200) which I really would like to avoid.
private void OnTakeSnapshot(object sender, RoutedEventArgs e)
{
this.LayoutUpdated += RunSnapshotMgr;
this.Hide();
}
private void RunSnapshotMgr(object sender, EventArgs e)
{
if (IsVisible == true) return;
Thread.Sleep(200);
SnapshotManager snapshotMgr = new SnapshotManager();
snapshotMgr.Closed += SnapshotMgrClosed;
snapshotMgr.Show();
this.LayoutUpdated -= RunSnapshotMgr;
}

scrollable wrap with overflow

I need to display a block of text in a resizable column. The text should wrap with overflow but, for a given column size, the user should be able to scroll horizontally to view the overflown text.
I do not believe this can be achieved w/ out of the box controls; if I'm wrong about that please tell me. I have attempted to achieve this with a custom control:
public class Sizer : ContentPresenter
{
static Sizer()
{
ContentPresenter.ContentProperty.OverrideMetadata(typeof(Sizer), new FrameworkPropertyMetadata(ContentChanged)); ;
}
public Sizer() : base() {}
protected override Size MeasureOverride(Size constraint)
{
var childWidth = Content==null ? 0.0 : ((FrameworkElement)Content).RenderSize.Width;
var newWidth = Math.Max(RenderSize.Width, childWidth);
return base.MeasureOverride(new Size(newWidth, constraint.Height));
}
private static void ContentChanged(DependencyObject dep, DependencyPropertyChangedEventArgs args)
{
var #this = dep as Sizer;
var newV = args.NewValue as FrameworkElement;
var oldV = args.OldValue as FrameworkElement;
if (oldV != null)
oldV.SizeChanged -= #this.childSizeChanged;
if(newV!=null)
newV.SizeChanged += #this.childSizeChanged;
}
private void childSizeChanged(object sender, SizeChangedEventArgs e)
{
this.InvalidateMeasure();
}
}
...and I can test it in a simple WPF application like so:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Auto" VerticalAlignment="Top">
<local:Sizer>
<local:Sizer.Content>
<TextBlock Background="Coral" Text="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaa"
TextWrapping="WrapWithOverflow" />
</local:Sizer.Content>
</local:Sizer>
</ScrollViewer>
<GridSplitter Grid.Column="1" Background="Black" VerticalAlignment="Stretch" HorizontalAlignment="Left" Width="5" />
</Grid>
This works, after a fashion. It wraps and displays the text correctly, and the horizontal scrollbar lets me view the overflown text. If I resize the column to the right (larger) the text re-wraps correctly. However, if I resize the column to the left (smaller) the text will not re-wrap. So, for instance, if I resize the column to the right so that all the text is on one line it will remain all on one line regardless of any subsequent re-sizing. This is an unacceptable bug.
I have tinkered w/ this code a great deal although I haven't had what you'd a call a good strategy for finding a solution. I do not know and have not been able to discover any mechanism for forcing a textblock to re-wrap its contents. Any advice?
I was able to get this working (w/ some limitations) by adding the following to the custom control:
//stores first ScrollViewer ancestor
private ScrollViewer parentSV = null;
//stores collection of observed valid widths
private SortedSet<double> wrapPoints = new SortedSet<double>();
protected override void OnVisualParentChanged(DependencyObject oldParent)
{
parentSV = this.FindParent<ScrollViewer>();
base.OnVisualParentChanged(oldParent);
}
... and editing MeasureOverride list this:
protected override Size MeasureOverride(Size constraint)
{
if (parentSV != null)
{
var childWidth = Content == null ? 0.0 : ((FrameworkElement)Content).RenderSize.Width;
var viewportWidth = parentSV.ViewportWidth;
if (childWidth > viewportWidth + 5)
wrapPoints.Add(childWidth);
var pt = wrapPoints.FirstOrDefault(d => d > viewportWidth);
if (pt < childWidth)
childWidth = pt;
return base.MeasureOverride(new Size(childWidth, constraint.Height));
}
else
return base.MeasureOverride(constraint);
}
I do not like this at all. It doesn't work for WrapPanels (although that isn't a required use case for me), it flickers occasionally and I'm concerned that the wrapPoints collection may grow very large. But it does what I need it to do.

WPF DragEnter between two Canvas not firing

I am trying to drag an item from one Canvas to another. I want an event to fire when the object enters the other Canvas. None of the Drag events seem to fire.
I have tried following the solution for this question, but it does not work for me:
Drag and Drop not responding as expected
My canvas is this:
<Window x:Class="DragEnterTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="DragEnterMainWindow" Height="460" Width="1000">
<Grid>
<Canvas Name="Toolbox" Background="Beige" Height="400" Width="200" Margin="12,12,800,35">
<Rectangle Name="dragRectangle" Canvas.Left="0" Canvas.Right="0" Width="50" Height="50" Fill="Red"
MouseLeftButtonDown="dragRectangle_MouseLeftButtonDown"
MouseLeftButtonUp="dragRectangle_MouseLeftButtonUp"
MouseMove="dragRectangle_MouseMove"
/>
</Canvas>
<Canvas Background="Azure" Height="400" Margin="218,12,0,35" Name="mainCanvas" Panel.ZIndex="-1"
DragEnter="mainCanvas_DragEnter"
DragLeave="mainCanvas_DragLeave"
PreviewDragEnter="mainCanvas_PreviewDragEnter"
PreviewDragLeave="mainCanvas_PreviewDragLeave"
AllowDrop="True"
DragDrop.Drop="mainCanvas_Drop"
/>
</Grid>
</Window>
If I do not have the Panel.ZIndex="-1" then the rectangle is dragged underneath the mainCanvas. This is true even if I set the ZIndex for the rectangle to some positive value.
My code is the following, modified by examples I have found:
namespace DragEnterTest
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private bool _isRectDragInProg;
public MainWindow()
{
InitializeComponent();
}
private void dragRectangle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_isRectDragInProg = true;
dragRectangle.CaptureMouse();
}
private void dragRectangle_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
_isRectDragInProg = false;
dragRectangle.ReleaseMouseCapture();
}
private void dragRectangle_MouseMove(object sender, MouseEventArgs e)
{
if (!_isRectDragInProg) return;
// get the position of the mouse relative to the Canvas
var mousePos = e.GetPosition(Toolbox);
// center the rect on the mouse
double left = mousePos.X - (dragRectangle.ActualWidth / 2);
double top = mousePos.Y - (dragRectangle.ActualHeight / 2);
Canvas.SetLeft(dragRectangle, left);
Canvas.SetTop(dragRectangle, top);
}
private void mainCanvas_DragEnter(object sender, DragEventArgs e)
{
string t = "Test"; // Never enters this event
}
private void mainCanvas_DragLeave(object sender, DragEventArgs e)
{
string t = "Test"; // Never enters this event
}
private void mainCanvas_PreviewDragEnter(object sender, DragEventArgs e)
{
string t = "Test"; // Never enters this event
}
private void mainCanvas_PreviewDragLeave(object sender, DragEventArgs e)
{
string t = "Test"; // Never enters this event
}
private void mainCanvas_Drop(object sender, DragEventArgs e)
{
string t = "Test"; // Never enters this event
}
}
}
You're not dragging anything actually, just moving rectangle here and there in canvas.
You'll need to call DragDrop.DoDragDrop function when your rectangle leave the source, also detach it from source so that you can add it to target later.
// Drag - In mousemove event when mouse has gone out of toolbox
DragDrop.DoDragDrop(
Toolbox,
new DataObject("MyWPFObject", rectangle),
DragDropEffects.Move
);
// Drop - In Drop event of target
if (e.Data.GetDataPresent("MyWPFObject"))
{
var rectangle = e.Data.GetData("MyWPFObject") as Rectangle
....
Tutorial ...
I believe it's because you're capturing the mouse, so all mouse events are being handled by your dragged Rectangle, and they don't get passed on to your Canvas
I personally had all kinds of problems with using WPF's built-in drag/drop functionality, so I ended up using MouseEvents instead.
The solution I used was from this answer, and went like this:
On MouseDown with left button down, record the position (on MouseLeave erase the position)
On MouseMove, if left button down, position is recorded, and current mouse position differs by more than delta, set a flag saying drag operation is in progress & have your application (not the dragged object) capture the mouse
On MouseMove with drag operation in progress, use hit testing to determine where your rectangle should be (ignoring the rectangle itself) and adjust its parenting and position accordingly.
On MouseUp with drag operation in progress, release the mouse capture and clear the "drag operation is in progress" flag

ScaleTransform transforms non-linearly

I am using scale transform to allow a user to resize a control. What happens though is that when you start to move the mouse the control jumps to a new size, and then scales oddly. The further you move your mouse from the starting location the larger the increase in size becomes.
I expect its the way I calculate the scale to be applied. Here is the code:
private void ResizeGrip_MouseDown(object sender, MouseButtonEventArgs e)
{
ResizeHandle.CaptureMouse();
//Get the initial coordinate cursor location on the window
initBtmX = e.GetPosition(this).X;
bottomResize = true;
}
private void ResizeGrip_MouseUp(object sender, MouseButtonEventArgs e)
{
bottomResize = false;
ResizeHandle.ReleaseMouseCapture();
}
private void ResizeGrip_MouseMove(object sender, MouseEventArgs e)
{
if( bottomResize == true)
{
//Get the new Y coordinate cursor location
double newBtmX = e.GetPosition(this).X;
//Get the smallest change between the initial and new cursor location
double diffX = initBtmX - newBtmX;
// Let our rectangle capture the mouse
ResizeHandle.CaptureMouse();
double newWidth = e.GetPosition(this).X - diffX;
double scaler = newWidth / ResizeContainer.ActualWidth;
Console.WriteLine("newWidth: {0}, scalar: {1}", newWidth, scaler);
if (scaler < 0.75 || scaler > 3)
return;
ScaleTransform scale = new ScaleTransform(scaler, scaler);
ResizeContainer.LayoutTransform = scale;
}
}
Update: Now with XAML
<wtk:IToolDialog x:Name="VideoPlayer" ParentControl="{Binding ElementName=Stage}" DialogTitle="Video Player" Margin="90,5,0,0">
<Grid>
<Grid x:Name="ResizeContainer" ClipToBounds="True" Width="320" Height="240" HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,1">
<!-- The video frame -->
<Image Stretch="Fill" Source="{Binding CurrentFrameImage}" x:Name="VideoImage" />
<Grid>
<pplcontrols:VideoGroundPlane Foreground="Black" GridSize="20" GroundPlane="{Binding GroundPlane}">
</pplcontrols:VideoGroundPlane>
</Grid>
<Grid x:Name="HitMask" IsHitTestVisible="False"/>
</Grid>
<ResizeGrip Cursor="SizeNWSE" x:Name="ResizeHandle" VerticalAlignment="Bottom" HorizontalAlignment="Right" Mouse.MouseDown="ResizeGrip_MouseDown" Mouse.MouseUp="ResizeGrip_MouseUp" Mouse.MouseMove="ResizeGrip_MouseMove"></ResizeGrip>
</Grid>
</wtk:IToolDialog>
Any reason why you are not using ResizeContainer.RenderTransfrom instead of ResizeContainer.LayoutTransform? I.e. use
ResizeContainer.LayoutTransform = scale;
If you want the scale to be linear I think you should use
double scaler = 1 - diff / ResizeContainer.ActualWidth;
EDIT:
There is a bug in the code that causes the scaled control to "jump" in size if you try to resize more than once. I suggest you do the following:
Add a RenderTransform to your ResizeContainer grid:
<Grid.RenderTransform>
<ScaleTransform x:Name="transform" ScaleX="1" ScaleY="1" />
</Grid.RenderTransform>
Change the code in your MouseMove event handler to the following:
if (bottomResize)
{
double newBtmX = e.GetPosition(this).X;
double scaler = -(initBtmX - newBtmX) / grid1.ActualWidth;
initBtmX = newBtmX;
transform.ScaleX += scaler;
transform.ScaleY += scaler;
}
This way you change the scale by whatever small amount the mouse has moved while dragging. All child controls within the grid should scale as well.

Resources