SizeToContent with a Listbox - wpf

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);
}

Related

How to exactly center rendered text inside a Border

I have a Border with a Content of TextBlock that I want to be perfectly centered both horizontally and vertically. No matter what I try it never looks centered. What am I missing?
Using the code below the top of the text is 19px below the border, the bottom of the text is 5px above the border. It's also off center left or right depending on the Text value which I assume is related to the font.
The solution should work for varying text (1-31) with any font.
Code
<Grid Width="50" Height="50">
<Border BorderThickness="1" BorderBrush="Black">
<TextBlock Text="13" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="50"/>
</Border>
</Grid>
Result
Well then, challenge accepted ;-) This solution is based on the following idea:
Fit the TextBlock inside the border and make sure the entire text is rendered, even if not visible.
Render the text into a bitmap.
Detect the glyphs (i.e. characters) inside the bitmap to get the pixel-exact position.
Update the UI layout so the text is centered inside the border.
If possible, allow simple, generic usage.
1. TextBlock inside border / fully rendered
This is simple once you realize that the entire content of a ScrollViewer is rendered, so here is my UserControl XAML:
<UserControl x:Class="WpfApplication4.CenteredText"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<ScrollViewer x:Name="scroll"
IsHitTestVisible="False"
VerticalScrollBarVisibility="Hidden"
HorizontalScrollBarVisibility="Hidden" />
</Grid>
</UserControl>
With the code behind as:
public partial class CenteredText : UserControl
{
public CenteredText()
{
InitializeComponent();
}
public static readonly DependencyProperty ElementProperty = DependencyProperty
.Register("Element", typeof(FrameworkElement), typeof(CenteredText),
new PropertyMetadata(OnElementChanged));
private static void OnElementChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var elem = e.NewValue as FrameworkElement;
var ct = d as CenteredText;
if(elem != null)
{
elem.Loaded += ct.Content_Loaded;
ct.scroll.Content = elem;
}
}
public FrameworkElement Element
{
get { return (FrameworkElement)GetValue(ElementProperty); }
set { SetValue(ElementProperty, value); }
}
void Content_Loaded(object sender, RoutedEventArgs e) /*...*/
}
This control is basically a ContentControlwhich allows to handle the Loaded event of the content generically. There may be a simpler way to do this, I'm not sure.
2. Render to Bitmap
This one is simple. In the Content_Loaded() method:
void Content_Loaded(object sender, RoutedEventArgs e)
{
FrameworkElement elem = sender as FrameworkElement;
int w = (int)elem.ActualWidth;
int h = (int)elem.ActualHeight;
var rtb = new RenderTargetBitmap(w, h, 96, 96, PixelFormats.Pbgra32);
rtb.Render(elem);
/* glyph detection ... */
}
3. Detect the glyphs
This is surprisingly easy since a TextBlock is rendered with fully transparent background by default and we are only interested in bounding rectangle. This is done in a separate method:
bool TryFindGlyphs(BitmapSource src, out Rect rc)
{
int left = int.MaxValue;
int toRight = -1;
int top = int.MaxValue;
int toBottom = -1;
int w = src.PixelWidth;
int h = src.PixelHeight;
uint[] buf = new uint[w * h];
src.CopyPixels(buf, w * sizeof(uint), 0);
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
// background is assumed to be fully transparent, i.e. 0x00000000 in Pbgra
if (buf[x + y * w] != 0)
{
if (x < left) left = x;
if (x > toRight) toRight = x;
if (y < top) top = y;
if (y > toBottom) toBottom = y;
}
}
}
rc = new Rect(left, top, toRight - left, toBottom - top);
return (toRight > left) && (toBottom > top);
}
The above method tries to find the leftmost, rightmost, topmost and bottommost pixel which is not transparent and returns the results as a Rect in the output parameter.
4. Update Layout
This is done later in the Content_Loaded method:
void Content_Loaded(object sender, RoutedEventArgs e)
{
/* render to bitmap ... */
Rect rc;
if (TryFindGlyphs(rtb, out rc))
{
if (rc.Height > this.scroll.ActualHeight || rc.Width > this.scroll.ActualWidth)
{
return; // todo: error handling
}
double desiredV = rc.Top - 0.5 * (this.scroll.ActualHeight - rc.Height);
double desiredH = rc.Left - 0.5 * (this.scroll.ActualWidth - rc.Width);
if (desiredV > 0)
{
this.scroll.ScrollToVerticalOffset(desiredV);
}
else
{
elem.Margin = new Thickness(elem.Margin.Left, elem.Margin.Top - desiredV,
elem.Margin.Right, elem.Margin.Bottom);
}
if (desiredH > 0)
{
this.scroll.ScrollToHorizontalOffset(desiredH);
}
else
{
elem.Margin = new Thickness(elem.Margin.Left - desiredH, elem.Margin.Top,
elem.Margin.Right, elem.Margin.Bottom);
}
}
}
This UI is updated using the following strategy:
Compute the desired offset between the border and the glyph rectangle in both directions
If the desired offset is positive, it means that the text needs to move up (or left in the horizontal case) so we can scroll down (right) by the desired offset.
If the desired offset is negative, it means that the text needs to move down (or right in the horizontal case). This cannot be done by scrolling since the TextBlock is top-left-aligned (by default) and the ScrollViewer is still at the initial (top/left) position. There is a simple solution though: Add the desired offset to the Margin of the TextBlock.
5. Simple Usage
The CenteredText control is used as follows:
<Border BorderBrush="Black" BorderThickness="1" Width="150" Height="150">
<local:CenteredText>
<local:CenteredText.Element>
<TextBlock Text="31" FontSize="150" />
</local:CenteredText.Element>
</local:CenteredText>
</Border>
Results
For border size 150x150 and FontSize 150:
For border size 150x150 and FontSize 50:
For border size 50x50 and FontSize 50:
Note: There is a 1-pixel error where the space to the left of the text is 1 pixel thicker or thinner than the space to the right. Same with the top / bottom spacing. This happens if the border has an even width and the rendered text an odd width (no sub-pixel perfectness is provided, sorry)
Conclusion
The presented solution should work up to a 1-pixel error with any Font, FontSize and Text and is simple to use.
And if you haven't noticed yet, very limited assumptions were made about the FrameworkElement which is used with the Elem property of the CenteredText control. So this should also work with any element which has transparent background and needs (near-)perfect centering.
What you are talking about is related to the specific font (and characters within that font) that you are using. Different fonts will have different baselines, heights and other attributes. In order to combat that, just use Padding on the Border or Margin on the TextBlock to make it fit where you want it:
<Grid Width="50" Height="50">
<Border BorderThickness="1" BorderBrush="Black">
<TextBlock Text="13" VerticalAlignment="Center" HorizontalAlignment="Center"
FontSize="50" Margin="0,0,3,14" />
</Border>
</Grid>
Note: You can also use the TextBlock.TextAlignment Property to make adjustments to the horizontal alignment of text content.
I'd add this as a comment but I haven't got enough reputation :P
It looks off center because the height and width you have specified for the grid (50x50) is too small to house a font size of 50. Either increase the size to 100x100 or lower the font size to something smaller.
To demonstrate that they will be perfectly aligned in the center by doing this - view this code in visual studio somewhere. You will see the numbers of these textblocks overlap perfectly.
<Grid Height="100" Width="100">
<Border BorderThickness="1" BorderBrush="Black" >
<TextBlock Text="13" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="50"/>
</Border>
<Border BorderThickness="1" BorderBrush="Black" >
<TextBlock Text="31" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="50"/>
</Border>
</Grid>
I hope this helps you out :)

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.

Track WPF Grid Column Width Upon Resizing

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/

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