I my WPF app I have a relatively small box of limited width, where I need to display some text which has been entered by the user. The text can realistically be expected to be between one and five words, but the words can easily be larger than the box.
If the text is too long, but contains multiple words which can be broken up into lines, I'd want the text to wrap. However, if any single word is too large to fit, then I want the text size to shrink until that word is small enough to fit, regardless of whether or not that text is also wrapping. I don't care how much vertical space the text takes up.
Here's an example I put together manually in Excel to demonstrate the intended behavior:
In example 1 the whole text fits in the box.
In example 2 the text is two words, so it can be wrapped without shrinking the text.
In example 3, the single word is too long so the text has to be shrunk.
In example 4 the text can be wrapped but it still contains a word that is too long to fit, so the text has to be shrunk until that longest word can fit.
How can I accomplish this in WPF? I haven't been able to find a combination of ViewBox and TextBlock.TextWrapping which does this.
EDIT:
If I do have to do this manually (which would be a bit of a nightmare), then is there at least a way I can figure out what the TextBlock decides is a "line"? I would need to know how it's going to break up the text before I could identify if any one "line" is going to be too long.
You have to do this manually. The following code sample adjusts the font size of a TextBox until all text fits in to the viewport (maximum available space for text rendering). You have to execute this method from an event handler that is registered to the TextBoxBase.TextChanged event:
protected void ResizeTextToFit(TextBox textBox)
{
// Make sure the first line is always visible
textBox.ScrollToVerticalOffset(0);
bool fontSizeHasChanged = false;
// Shrink to fit as long
// the last visible line is not the last line or
// the true text height is bigger than the visible text height
// and prevent font size to be set to '0'
while (textBox.FontSize > 1
&& (textBox.GetLastVisibleLineIndex() < textBox.LineCount - 1
|| textBox.ExtentHeight > textBox.ViewportHeight))
{
fontSizeHasChanged = true;
textBox.FontSize -= 1.0;
}
if (fontSizeHasChanged)
{
return;
}
// Enlarge to fit as long the last line is visible
// and the text height fits into the viewport
while (textBox.GetLastVisibleLineIndex() == textBox.LineCount - 1
&& textBox.ExtentHeight < textBox.ViewportHeight)
{
textBox.FontSize += 1.0;
}
textBox.FontSize -= 1.0;
}
You may prefer to extend your own class from TextBox to encapsulate this behavior.
This example depends on a TextBox that has a fixed Width and Height, so that it cann't resize to the content.
Seeing as no real solution to this exists, I ended up coding it myself:
Imports System.ComponentModel
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Documents
Public Class TextScalerBehavior
Public Shared ReadOnly ShrinkToFitProperty As DependencyProperty = DependencyProperty.RegisterAttached("ShrinkToFit", GetType(Boolean), GetType(TextScalerBehavior), New PropertyMetadata(False, New PropertyChangedCallback(AddressOf ShrinkToFitChanged)))
Public Shared Function GetShrinkToFit(obj As TextBlock) As Boolean
Return obj.GetValue(ShrinkToFitProperty)
End Function
Public Shared Sub SetShrinkToFit(obj As TextBlock, value As Boolean)
obj.SetValue(ShrinkToFitProperty, value)
End Sub
Protected Shared Sub ShrinkToFitChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
Dim tb As TextBlock = d
If e.NewValue Then
tb.AddHandler(TextBlock.SizeChangedEvent, TargetSizeChangedEventHandler)
With DependencyPropertyDescriptor.FromProperty(TextBlock.TextProperty, GetType(TextBlock))
.AddValueChanged(tb, TargetTextChangedEventHandler)
End With
tb.AddHandler(TextBlock.LoadedEvent, TargetLoadedEventHandler)
Else
tb.RemoveHandler(TextBlock.SizeChangedEvent, TargetSizeChangedEventHandler)
With DependencyPropertyDescriptor.FromProperty(TextBlock.TextProperty, GetType(TextBlock))
.RemoveValueChanged(tb, TargetTextChangedEventHandler)
End With
tb.RemoveHandler(TextBlock.LoadedEvent, TargetLoadedEventHandler)
End If
End Sub
Protected Shared ReadOnly TargetSizeChangedEventHandler As New RoutedEventHandler(AddressOf TargetSizeChanged)
Protected Shared Sub TargetSizeChanged(Target As TextBlock, e As RoutedEventArgs)
Update(Target)
End Sub
Protected Shared ReadOnly TargetTextChangedEventHandler As New EventHandler(AddressOf TargetTextChanged)
Protected Shared Sub TargetTextChanged(Target As TextBlock, e As EventArgs)
Update(Target)
End Sub
Protected Shared ReadOnly TargetLoadedEventHandler As New RoutedEventHandler(AddressOf TargetLoaded)
Protected Shared Sub TargetLoaded(Target As TextBlock, e As RoutedEventArgs)
Update(Target)
End Sub
Private Shared ReadOnly Shrinkging As New HashSet(Of TextBlock)
Protected Shared Sub Update(Target As TextBlock)
If Target.IsLoaded Then
Dim Clip = Primitives.LayoutInformation.GetLayoutClip(Target)
If Clip IsNot Nothing Then
If Not Shrinkging.Contains(Target) Then Shrinkging.Add(Target)
Target.FontSize -= 1
ElseIf Target.FontSize < TextElement.GetFontSize(Target.Parent) Then
If Shrinkging.Contains(Target) Then
Shrinkging.Remove(Target)
Else
Target.FontSize += 1
End If
End If
End If
End Sub
End Class
This class implements the behavior I need as a WPF attached behavior using attached dependency properties. The magic happens in the final routine: Update.
In WPF, if a given element is being clipped (i.e. it's bigger than the space it's allowed to take up, so it gets cut off), then LayoutInformation.GetLayoutClip returns data on what area of the element is visible. If an element is not clipped, this seems to return null (though the docs don't say that).
A TextBlock with TextWrapping="WrapWithOverflow" will "overflow" past the edges of its container if any single line is too big to be broken correctly.
The Update routine checks to see if this clipping is occurring and if so lowers the font size by 1. This changes the size of the TextBlock and triggers another round of Update, which continues the cycle until the element no longer clips.
There is also additional logic to scale the font back up to its original size if the available space increases.
An example of usage is:
<TextBlock [YourNamespace]:TextScalerBehavior.ShrinkToFit="True" TextWrapping="WrapWithOverflow"/>
Remember that TextWrapping="WrapWithOverflow" is required.
Related
I have a WPF app with a FlowDocumentReader control and when I click the search button to search the text, the text box doesn't match the theme of the app. The text does, but the background of the text box doesn't. So if I am using a dark theme, the text I type into the text box is white (which is correct), but the text box background is also white (incorrect, making text not legible).
Does anyone know how to fix this? I tried applying a style but I don't know which component to target.
Wow, Microsoft really does not make this easy.
My Process
I tried the easy trick of adding a Style TargeType="TextBox" to FlowDocumentReader.Resources, but that doesn't work.
I tried doing things the "right" way and overriding FlowDocumentReader's ControlTemplate, but the TextBox in question isn't even part of the ControlTemaplte! Instead, there's a Border named PART_FindToolBarHost. The TextBox we want is added as a child to PART_FindToolBarHost in code- but only after the user has clicked the "find" button (the one with the magnifying glass icon). You can see this for yourself by looking at the control's source code.
With no more XAML-only ideas, I had to resort to using code. We need to somehow get a reference to the TextBox being created, and I can't think of any better way than to manually search the visual tree for it. This is complicated by the fact that the TextBox only exists once the find command has been executed.
Specifically, the find button in FlowDocumentReader binds to ApplicationCommands.Find. I tried adding a CommandBinding for that command to FlowDocumentReader so I could use it as a trigger to retrieve the TextBox. Unfortunately, adding such a CommandBinding somehow breaks the built-in functionality and prevents the TextBox from being generated at all. This breaks even if you set e.Handled = False.
Luckily, though, FlowDocumentReader exposes an OnFindCommand- except, of course, it's a Protected method. So I finally gave in and decided to inherit FlowDocumentReader. OnFindCommand works as a reliable trigger, but it turns out the TextBox isn't created until after the sub finishes. I was forced to use Dispatcher.BeginInvoke in order to schedule a method to run after the TextBox was actually added.
Using OnFindCommand as a trigger, I was finally able to reliably get a reference to the "find" TextBox, which is actually named FindTextBox.
Now that I can get a reference, we can apply our own Style. Except: FindTextBox already has a Style, so unless we want to override it, we're going to have to merge the two Styles. There's no publicly-accessible method for this (even though WPF does this internally in places), but luckily I already had some code for this.
The Working Code
First, a Module with the helper methods I used:
FindVisualChild is used to loop through the visual tree and get a reference to FindTextBox.
MergeStyles is used to combine the existing Style with the Style we supply, once we have that reference.
Module OtherMethods
<Extension()>
Public Function FindVisualChild(obj As DependencyObject, Name As String) As FrameworkElement
For i As Integer = 0 To VisualTreeHelper.GetChildrenCount(obj) - 1
Dim ChildObj As DependencyObject = VisualTreeHelper.GetChild(obj, i)
If TypeOf ChildObj Is FrameworkElement AndAlso DirectCast(ChildObj, FrameworkElement).Name = Name Then Return ChildObj
ChildObj = FindVisualChild(ChildObj, Name)
If ChildObj IsNot Nothing Then Return ChildObj
Next
Return Nothing
End Function
Public Function MergeStyles(ByVal style1 As Style, ByVal style2 As Style) As Style
Dim R As New Style
If style1 Is Nothing Then Throw New ArgumentNullException("style1")
If style2 Is Nothing Then Throw New ArgumentNullException("style2")
If style2.BasedOn IsNot Nothing Then style1 = MergeStyles(style1, style2.BasedOn)
For Each currentSetter As SetterBase In style1.Setters
R.Setters.Add(currentSetter)
Next
For Each currentTrigger As TriggerBase In style1.Triggers
R.Triggers.Add(currentTrigger)
Next
For Each key As Object In style1.Resources.Keys
R.Resources(key) = style1.Resources(key)
Next
For Each currentSetter As SetterBase In style2.Setters
R.Setters.Add(currentSetter)
Next
For Each currentTrigger As TriggerBase In style2.Triggers
R.Triggers.Add(currentTrigger)
Next
For Each key As Object In style2.Resources.Keys
R.Resources(key) = style2.Resources(key)
Next
Return R
End Function
End Module
Then, there's StyleableFlowDocumentReader, which is what I named my extended control that inherits FlowDocumentReader:
Public Class StyleableFlowDocumentReader
Inherits FlowDocumentReader
Protected Overrides Sub OnFindCommand()
MyBase.OnFindCommand()
Dispatcher.BeginInvoke(Sub() GetFindTextBox(), DispatcherPriority.Render)
End Sub
Private Sub GetFindTextBox()
findTextBox = Me.FindVisualChild("FindTextBox")
ApplyFindTextBoxStyle()
End Sub
Private Sub ApplyFindTextBoxStyle()
If findTextBox IsNot Nothing Then
If findTextBox.Style IsNot Nothing AndAlso FindTextBoxStyle IsNot Nothing Then
findTextBox.Style = MergeStyles(findTextBox.Style, FindTextBoxStyle)
Else
findTextBox.Style = If(FindTextBoxStyle, findTextBox.Style)
End If
End If
End Sub
Private findTextBox As TextBox
Public Property FindTextBoxStyle As Style
Get
Return GetValue(FindTextBoxStyleProperty)
End Get
Set(ByVal value As Style)
SetValue(FindTextBoxStyleProperty, value)
End Set
End Property
Public Shared ReadOnly FindTextBoxStyleProperty As DependencyProperty =
DependencyProperty.Register("FindTextBoxStyle",
GetType(Style), GetType(StyleableFlowDocumentReader),
New PropertyMetadata(Nothing, Sub(d, e) DirectCast(d, StyleableFlowDocumentReader).ApplyFindTextBoxStyle()))
End Class
And then, finally, a usage example:
<local:StyleableFlowDocumentReader x:Name="Reader">
<local:StyleableFlowDocumentReader.FindTextBoxStyle>
<Style TargetType="TextBox">
<Setter Property="Foreground" Value="Blue"/>
</Style>
</local:StyleableFlowDocumentReader.FindTextBoxStyle>
<FlowDocument/>
</local:StyleableFlowDocumentReader>
I paint the content of a cell for example an icon and text. If the text does not fit the cell bounds it is drawed with ellipsis. How can I tell the DataGridView that the cell needs more space to handle autosizing correct?
There are various options presented in [Customizing Content-based Sizing Behavior](Customizing Content-based Sizing Behavior); I recommend that you override the DataGridViewCell.GetPreferredSize Method to provide custom cell size data to the specific DataGridViewColumn's CellTemplate Property.
Since you have not detailed your particular design conditions, I will assume that the the DataGridViewColumn is a DataGridViewTextBoxColumn.
The custom cell definition would be something similar to the following.
Public Class CustomSizedTextBoxCell : Inherits DataGridViewTextBoxCell
Protected Overrides Function GetPreferredSize(graphics As Graphics, cellStyle As DataGridViewCellStyle, rowIndex As Int32, constraintSize As Size) As Size
Dim ret As Size = MyBase.GetPreferredSize(graphics, cellStyle, rowIndex, constraintSize)
' this simple example just doubles the width of the base preferred size
' replace this with logic that satifies your requirements
ret.Width *= 2
Return ret
End Function
End Class
The to use this custom cell:
DataGridView1.Columns(0).CellTemplate = New CustomSizedTextBoxCell()
I am programmatically creating a GridViewColumn in WPF as follows:
Dim oGVCol As GridViewColumn = New GridViewColumn
Dim oHeaderTemplate As DataTemplate
oHeaderTemplate = New DataTemplate
Dim oGridFactory As FrameworkElementFactory = New FrameworkElementFactory(GetType(Grid))
oHeaderTemplate.VisualTree = oGridFactory
oGridFactory.SetValue(Grid.BackgroundProperty, Brushes.Transparent)
oGVCol.HeaderTemplate = oHeaderTemplate
(I have removed irrelevant code to set the content of the grid)
What I can't figure out is how to add a "click" event for the GridViewColumnHeader itself. I can add events to the Grid and any other Controls I added through the Factory objects, no problem. But I'm stuck on how to add an event to the header itself.
If you have a solution in VB.NET, great, but C# is fine too.
One (failed) attempt:
AddHandler TryCast(oGVCol.Header, GridViewColumnHeader).Click, AddressOf HeaderClick
Sadly it turns out that I cannot cast oGVCol.Header to a GridViewColumnHeader.
Ok, it may not be pretty, but I found a pretty decent solution to the problem.
Firstly, when I create the Grid Factory for the root element in the header's Visual Tree, I give it a name
oGridFactory.SetValue(Grid.NameProperty, <column name here>))
(Please note that the Names must have only letters, numbers and underscores so if your data contains column names that don't have those, you'll need to deal with that both here, to convert invalid names to valid ones, and below, to revert them back to their original names if necessary.... I won't detail that functionality here)
Also, I add an event handler to the Root "Grid" in the Template for the column header:
oGridFactory.AddHandler(Grid.SizeChangedEvent,
New SizeChangedEventHandler(AddressOf ColumnHeaderSizeChanged))
The "magic" happens in the procedure ColumnHeaderSizeChanged. This procedure is called both when the grid is Rendered the first time, but also when the user is manually resizing columns.
Signature:
Private Sub ColumnHeaderSizeChanged(sender As Object, e As SizeChangedEventArgs)
I keep a List(Of GridViewColumnHeaders) which is reset to an empty list when I need to replace the Grid with a different one. In the ColumnHeaderSizeChanged event I then do the following:
The first thing we need to do is get to the Root of the controls in the Column Header. For example, your column header may contain a TextBlock to show a column name, and icons to indicate it's been sorted up or down. That sort of thing. When the user clicks on the header they may be clicking on any of those controls, so:
Dim oParent As Object
Dim oColHeader As GridViewColumnHeader = Nothing
Dim sColHeaderName As String = String.Empty
Dim oGWH As Grid = Nothing
oParent = e.OriginalSource 'This may be any of the controls in the header.
If Not oParent Is Nothing Then
Try
While Not oParent.Parent Is Nothing
'So we keep going down the Tree until we hit the Root Parent
'which will be the main Grid created in the Grid Factory
oParent = oParent.Parent
End While
Catch
End Try
End If
'But at this point, if we still have a control, it will be the main Grid
If oParent Is Nothing Then
Exit Sub
End If
If TryCast(oParent, Grid) Is Nothing Then
'what the heck is this? This SHOULD be the Grid at the root of the Visual Tree,
'so if, for whatever reason, this is NOT a Grid, get outta here.
Exit Sub
End If
By this point we're on the main Grid, but now we need to get the GridViewColumnHeader into which this Grid has been created. So now we go to the TemplatedParent
While Not oParent.TemplatedParent Is Nothing
oParent = oParent.TemplatedParent
oColHeader = TryCast(oParent, GridViewColumnHeader)
If Not oColHeader Is Nothing Then
'This procedure is called both when the Grid View is first rendered,
'and when the user is dragging the column width.
If Mouse.LeftButton = MouseButtonState.Pressed Then
'Do something appropriate to when the user is resizing the column
Else
'Do something appropriate to when the grid
'is first Rendered or re-displayed
End If
Exit While
End If
End While
At this point we have the GridViewColumnHeader we need, so we can add it to the List and add a Handler for its Click event. moColHeaders is the List(Of GridViewColumnHeaders)
If Not oColHeader Is Nothing Then
If Not moColHeaders.Contains(oColHeader) Then
oColHeader.Name = <column name here>
moColHeaders.Add(oColHeader) 'Only need to add it once!
AddHandler oColHeader.Click, AddressOf HeaderClick
End If
End If
Now you can code your HeaderClick procedure to handle the GridViewColumnHeader.Click event.
Signature:
Private Sub HeaderClick(sender As Object, e As RoutedEventArgs)
this relates to my earlier question - I want to animate a grid splitter (to make panels slide into / out of view). We are pretty good at VB and already have a VB project, so would like to stay with VB if we can, but most WPF examples seem to be in XAML or CS.
I have some simple VB animation code working, BUT:
Of course, what needs to be animated is the width / height of the grid column / row, and this is not a dependency property. I found some clever stuff in CS to make a dependency property but could not translate this to vb. So I found a simple workaround which is to animate a dockpanel in the grid cell, catch it's size changed events and use these to set the cell grid size. It works but I wonder if it's less efficient as 2 things are being changed separately? Also I have to (when the animation completes) set the grid cells sizes back to * in the right proportion, and the dockpanel size back to auto.
It works, but it seems a bit clumsy - does someone have an example of making the animation for the grid work directly from VB any any other suggestions?
Thanks
For reference, here is the VB code for a dependency property to animate a gridsplitter:
Public Class GridLengthAnimation
Inherits AnimationTimeline
Public Sub New()
End Sub
Public Property From() As GridLength
Get
Return DirectCast(GetValue(FromProperty), GridLength)
End Get
Set(value As GridLength)
SetValue(FromProperty, value)
End Set
End Property
Public Shared ReadOnly FromProperty As DependencyProperty
= DependencyProperty.Register("From", GetType(GridLength),
GetType(GridLengthAnimation))
Public Property [To]() As GridLength
Get
Return DirectCast(GetValue(ToProperty), GridLength)
End Get
Set(value As GridLength)
SetValue(ToProperty, value)
End Set
End Property
Public Shared ReadOnly ToProperty As DependencyProperty
= DependencyProperty.Register("To", GetType(GridLength),
GetType(GridLengthAnimation))
Public Overrides ReadOnly Property TargetPropertyType() As Type
Get
Return GetType(GridLength)
End Get
End Property
Protected Overrides Function CreateInstanceCore() As Freezable
Return New GridLengthAnimation()
End Function
Public Overrides Function GetCurrentValue
(defaultOriginValue As Object,
defaultDestinationValue As Object,
animationClock As AnimationClock) As Object
Dim fromValue As Double = Me.From.Value
Dim toValue As Double = Me.[To].Value
If fromValue > toValue Then
Return New GridLength((1 - animationClock.CurrentProgress.Value)
* (fromValue - toValue) + toValue,
If(Me.[To].IsStar, GridUnitType.Star, GridUnitType.Pixel))
Else
Return New GridLength((animationClock.CurrentProgress.Value) *
(toValue - fromValue) + fromValue,
If(Me.[To].IsStar, GridUnitType.Star, GridUnitType.Pixel))
End If
End Function
End Class
I have a problem in getting ActualHeight and ActualWidth of image control in WPF. When user selects the image file, I want to resize the selected image based on the dimensions of the image control.
I tried to get the Image.ActualHeight and Image.ActualWidth when window initializes, but I found that both properties of Image control are '0'.
So how to get the dimensions of the image control.
the remarks for FrameworkElement.ActualHeight say that there might be some lag before the property has its real value.
This property is a calculated value
based on other height inputs, and the
layout system. The value is set by the
layout system itself, based on an
actual rendering pass, and may
therefore lag slightly behind the set
value of properties such as Height
that are the basis of the input
change.
The final size of your control is set by FrameworkElement.Arrange(-Override). You could override the method and just call the base class implementation. Its return value will be the actual size of your Image.
Off the top of my head, I think you should subscribe to the Load event on the image control, the ActualHeight/Width are not updated until that event fires.
The control's ActualSize is set after the "Measure" layout pass (the "Arrange" layout pass sets its location). The other two answers are helpful; the "Arrange" layout pass of the container only happens after its children have been measured, and the load handler of your image control should be called after its first layout pass has completed.
The Best solution I have found is to wait until after the Image has loaded.
Private Sub Update_imgImage(tURI As Uri)
imgImage.LayoutTransform = New ScaleTransform(scaleX:=1, scaleY:=1)
Dim src As BitmapImage = New BitmapImage()
src.BeginInit()
src.UriSource = tURI
src.CacheOption = BitmapCacheOption.OnLoad
src.EndInit()
imgImage.SetCurrentValue(Image.SourceProperty, src)
AddHandler src.DownloadCompleted, AddressOf ImageDownloadCompleted
End Sub
Then for ImageDownloadCompleted i have the following:
Sub ImageDownloadCompleted(sender As Object, e As System.EventArgs)
Dim src As BitmapImage
src = DirectCast(sender, BitmapImage)
Dim scaleXY As Double
If sender.Width = 0 Then Exit Sub
'default width is 600 for my item, if changed, then resize
If sender.Width <> 600 Then
scaleXY = 500 / sender.Width
imgImage.LayoutTransform = New ScaleTransform(scaleX:=scaleXY, scaleY:=scaleXY)
Else
imgImage.LayoutTransform = New ScaleTransform(scaleX:=1, scaleY:=1)
End If
RemoveHandler src.DownloadCompleted, AddressOf ImageDownloadCompleted
End Sub
I hope this works for you.