Skip to content

Commit d25d39c

Browse files
committed
Increased Version 1.1
Added ShowElipses property to show shortend text with ellipses (Left, Center, Right)
1 parent 2f08b9f commit d25d39c

6 files changed

Lines changed: 355 additions & 4 deletions

File tree

source/Demos/SuggestBoxTestLib/Views/DemoView.xaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,12 @@
145145
<RowDefinition Height="Auto"/>
146146
</Grid.RowDefinitions>
147147

148-
<bc:SuggestBox Grid.Column="0" Grid.Row="0"
148+
<bc:SuggestBox
149+
Grid.Column="0" Grid.Row="0"
149150
x:Name="DiskPathSuggestBox"
150151
Hint="Enter a file system path"
151152
DataContext="{Binding SuggestDirectory}"
153+
ShowElipses="Center"
152154
Text="{Binding CurrentText}"
153155
ValidText="{Binding IsValidText,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}"
154156
TextChangedCommand="{Binding SuggestTextChangedCommand}"
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
namespace SuggestBoxLib
2+
{
3+
using System;
4+
using System.Globalization;
5+
using System.Windows;
6+
using System.Windows.Controls;
7+
8+
/// <summary>
9+
/// Enum for specifying where the ellipsis should appear.
10+
/// </summary>
11+
public enum EllipsisPlacement
12+
{
13+
/// <summary>
14+
/// Do not show an ellipsis in PathTrimming TexBlock
15+
/// </summary>
16+
None,
17+
18+
/// <summary>
19+
/// Show an ellipsis in the center of PathTrimming TexBlock
20+
/// </summary>
21+
Center,
22+
23+
Left,
24+
25+
Right
26+
}
27+
28+
/// <summary>
29+
/// This PathTrimmingTextBlock textblock attaches itself to the events of a parent container and
30+
/// displays a trimmed path text when the size of the parent (container) is changed.
31+
///
32+
/// http://www.codeproject.com/Tips/467054/WPF-PathTrimmingTextBlock
33+
///
34+
/// Make sure you set, if you use this within an ListBox or ListView:
35+
/// ScrollViewer.HorizontalScrollBarVisibility="Disabled"
36+
/// </summary>
37+
public class PathTrimmingTextBlock : TextBlock
38+
{
39+
#region fields
40+
/// <summary>
41+
/// Path dependency property that stores the trimmed path
42+
/// </summary>
43+
private static readonly DependencyProperty PathProperty =
44+
DependencyProperty.Register("Path",
45+
typeof(string),
46+
typeof(PathTrimmingTextBlock),
47+
new UIPropertyMetadata(string.Empty));
48+
49+
50+
public EllipsisPlacement ShowElipses
51+
{
52+
get { return (EllipsisPlacement)GetValue(ShowElipsesProperty); }
53+
set { SetValue(ShowElipsesProperty, value); }
54+
}
55+
56+
public static readonly DependencyProperty ShowElipsesProperty =
57+
DependencyProperty.Register("ShowElipses", typeof(EllipsisPlacement),
58+
typeof(PathTrimmingTextBlock), new PropertyMetadata(EllipsisPlacement.None));
59+
60+
private FrameworkElement mContainer;
61+
#endregion fields
62+
63+
#region constructor
64+
/// <summary>
65+
/// Class Constructor
66+
/// </summary>
67+
public PathTrimmingTextBlock()
68+
{
69+
this.mContainer = null;
70+
71+
this.Loaded += new RoutedEventHandler(this.PathTrimmingTextBlock_Loaded);
72+
this.Unloaded += new RoutedEventHandler(this.PathTrimmingTextBlock_Unloaded);
73+
this.IsVisibleChanged += PathTrimmingTextBlock_IsVisibleChanged;
74+
}
75+
#endregion constructor
76+
77+
#region properties
78+
/// <summary>
79+
/// Path dependency property that stores the trimmed path
80+
/// </summary>
81+
public string Path
82+
{
83+
get { return (string)this.GetValue(PathProperty); }
84+
set { this.SetValue(PathProperty, value); }
85+
}
86+
#endregion properties
87+
88+
#region methods
89+
/// <summary>
90+
/// Textblock is constructed and start its live - lets attach to the
91+
/// size changed event handler of the containing parent.
92+
/// </summary>
93+
/// <param name="sender"></param>
94+
/// <param name="e"></param>
95+
private void PathTrimmingTextBlock_Loaded(object sender, RoutedEventArgs e)
96+
{
97+
FrameworkElement p = null;
98+
if (this.Parent is FrameworkElement)
99+
{
100+
p = (FrameworkElement)this.Parent;
101+
this.mContainer = p;
102+
}
103+
else
104+
{
105+
106+
if (this.Parent is DependencyObject dp)
107+
{
108+
for (DependencyObject parent = LogicalTreeHelper.GetParent(dp as DependencyObject);
109+
parent != null;
110+
parent = LogicalTreeHelper.GetParent(parent as DependencyObject))
111+
{
112+
p = parent as FrameworkElement;
113+
114+
if (p != null)
115+
break;
116+
}
117+
118+
this.mContainer = p;
119+
}
120+
}
121+
122+
if (this.mContainer != null)
123+
{
124+
this.mContainer.SizeChanged += new SizeChangedEventHandler(this.container_SizeChanged);
125+
126+
this.Text = this.GetTrimmedPath(this.mContainer.ActualWidth, this.ShowElipses);
127+
}
128+
//// else
129+
//// throw new InvalidOperationException("PathTrimmingTextBlock must have a container such as a Grid.");
130+
}
131+
132+
/// <summary>
133+
/// Remove custom event handlers and clean-up on unload.
134+
/// </summary>
135+
/// <param name="sender"></param>
136+
/// <param name="e"></param>
137+
private void PathTrimmingTextBlock_Unloaded(object sender, RoutedEventArgs e)
138+
{
139+
if (this.mContainer != null)
140+
this.mContainer.SizeChanged -= this.container_SizeChanged;
141+
}
142+
143+
/// <summary>
144+
/// Trim the containing text (path) accordingly whenever the parent container chnages its size.
145+
/// </summary>
146+
/// <param name="sender"></param>
147+
/// <param name="e"></param>
148+
private void container_SizeChanged(object sender, SizeChangedEventArgs e)
149+
{
150+
if (this.mContainer != null)
151+
this.Text = this.GetTrimmedPath(this.mContainer.ActualWidth, this.ShowElipses);
152+
}
153+
154+
/// <summary>
155+
/// Make sure we show the current string if visibility has changed to visible
156+
/// </summary>
157+
/// <param name="sender"></param>
158+
/// <param name="e"></param>
159+
private void PathTrimmingTextBlock_IsVisibleChanged(object sender,
160+
DependencyPropertyChangedEventArgs e)
161+
{
162+
if (this.mContainer != null && (bool)e.NewValue == true)
163+
{
164+
this.Text = this.GetTrimmedPath(this.mContainer.ActualWidth, this.ShowElipses);
165+
}
166+
}
167+
168+
/// <summary>
169+
/// Compute the text to display (with ellipsis) that fits the ActualWidth of the container
170+
/// </summary>
171+
/// <param name="width"></param>
172+
/// <param name="placement"></param>
173+
/// <returns></returns>
174+
private string GetTrimmedPath(double width,
175+
EllipsisPlacement placement)
176+
{
177+
string filename = string.Empty;
178+
string directory = string.Empty;
179+
180+
switch (placement)
181+
{
182+
// We don't want no ellipses to be shown for a string shortener
183+
case EllipsisPlacement.None:
184+
return this.Path;
185+
186+
// Try to show a nice ellipses somewhere in the middle of the string
187+
case EllipsisPlacement.Center:
188+
try
189+
{
190+
if (string.IsNullOrEmpty(this.Path) == false)
191+
{
192+
if (this.Path.Contains(string.Empty + System.IO.Path.DirectorySeparatorChar))
193+
{
194+
// Lets try to display the file name with priority
195+
filename = System.IO.Path.GetFileName(this.Path);
196+
directory = System.IO.Path.GetDirectoryName(this.Path);
197+
}
198+
else
199+
{
200+
// Cut this right in the middle since it does not seem to hold path info
201+
int len = this.Path.Length;
202+
int firstLen = this.Path.Length / 2;
203+
filename = this.Path.Substring(0, firstLen);
204+
205+
if (this.Path.Length >= (firstLen + 1))
206+
directory = this.Path.Substring(firstLen);
207+
}
208+
}
209+
else
210+
return string.Empty;
211+
}
212+
catch (Exception)
213+
{
214+
directory = this.Path;
215+
filename = string.Empty;
216+
}
217+
break;
218+
219+
case EllipsisPlacement.Left:
220+
directory = this.Path;
221+
filename = string.Empty;
222+
break;
223+
224+
case EllipsisPlacement.Right:
225+
directory = string.Empty;
226+
filename = this.Path;
227+
break;
228+
229+
default:
230+
throw new ArgumentOutOfRangeException(placement.ToString());
231+
}
232+
233+
bool widthOK = false;
234+
bool changedWidth = false;
235+
int indexString = 0;
236+
237+
if (placement == EllipsisPlacement.Left)
238+
indexString = 1;
239+
240+
TextBlock block = new TextBlock();
241+
block.Style = this.Style;
242+
block.FontWeight = this.FontWeight;
243+
block.FontStyle = this.FontStyle;
244+
block.FontStretch = this.FontStretch;
245+
block.FontSize = this.FontSize;
246+
block.FontFamily = this.FontFamily;
247+
248+
do
249+
{
250+
block.Text = FormatWith(placement, directory, filename);
251+
block.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
252+
253+
widthOK = block.DesiredSize.Width < width;
254+
255+
if (widthOK == false)
256+
{
257+
if (directory.Length == 0)
258+
{
259+
if (filename.Length > 0)
260+
{
261+
changedWidth = true;
262+
filename = filename.Substring(indexString, filename.Length - 1);
263+
}
264+
else
265+
return string.Empty;
266+
}
267+
else
268+
{
269+
changedWidth = true;
270+
directory = directory.Substring(indexString, directory.Length - 1);
271+
}
272+
}
273+
}
274+
while (widthOK == false);
275+
276+
if (changedWidth == false)
277+
return this.Path;
278+
279+
if (block != null) // Optimize for speed
280+
return block.Text;
281+
282+
return FormatWith(placement, directory, filename);
283+
}
284+
285+
/// <summary>
286+
/// Extend the string constructor with a string.Format like syntax.
287+
/// </summary>
288+
/// <param name="args"></param>
289+
/// <param name="placing"></param>
290+
/// <returns></returns>
291+
public static string FormatWith(EllipsisPlacement placing,
292+
params object[] args)
293+
{
294+
string formatString;
295+
296+
switch (placing)
297+
{
298+
case EllipsisPlacement.None:
299+
formatString = "{0}{1}";
300+
break;
301+
case EllipsisPlacement.Left:
302+
formatString = "...{0}{1}";
303+
break;
304+
case EllipsisPlacement.Center:
305+
formatString = "{0}...{1}";
306+
break;
307+
case EllipsisPlacement.Right:
308+
formatString = "{0}{1}...";
309+
break;
310+
default:
311+
throw new ArgumentOutOfRangeException(placing.ToString());
312+
}
313+
314+
return string.Format(CultureInfo.InvariantCulture, formatString, args);
315+
}
316+
#endregion methods
317+
}
318+
}

source/SuggestBoxLib/Properties/AssemblyInfo.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,5 @@
5151
// You can specify all the values or you can default the Build and Revision Numbers
5252
// by using the '*' as shown below:
5353
// [assembly: AssemblyVersion("1.0.*")]
54-
[assembly: AssemblyVersion("1.0.0.0")]
55-
[assembly: AssemblyFileVersion("1.0.0.0")]
54+
[assembly: AssemblyVersion("1.1.0.0")]
55+
[assembly: AssemblyFileVersion("1.1.0.0")]

source/SuggestBoxLib/SuggestBoxBase.xaml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,21 @@
2626
Foreground="{DynamicResource {x:Static reskeys:ResourceKeys.ControlTextBrushKey}}"
2727
Visibility="{Binding IsHintVisible, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource btv}}"
2828
Opacity="0.5" />
29-
<ScrollViewer Margin="0" x:Name="PART_ContentHost" RenderOptions.ClearTypeHint="Enabled" />
29+
30+
<ScrollViewer Margin="0"
31+
x:Name="PART_ContentHost"
32+
RenderOptions.ClearTypeHint="Enabled"
33+
Visibility="Hidden"
34+
/>
35+
36+
<bc:PathTrimmingTextBlock
37+
Path="{Binding Text, RelativeSource={RelativeSource TemplatedParent},UpdateSourceTrigger=PropertyChanged,Mode=OneWay}"
38+
ShowElipses="{Binding ShowElipses, RelativeSource={RelativeSource TemplatedParent},UpdateSourceTrigger=PropertyChanged,Mode=OneWay}"
39+
RenderOptions.ClearTypeHint="Enabled"
40+
x:Name="PART_EllipsedTEXT"
41+
Visibility="Visible"
42+
/>
43+
3044
<Popup x:Name="PART_Popup"
3145
AllowsTransparency="true"
3246
PlacementTarget="{Binding ElementName=PART_ContentHost}"
@@ -111,6 +125,8 @@
111125
</Trigger>
112126
<Trigger Property="IsFocused" Value="true">
113127
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource {x:Static reskeys:ResourceKeys.ControlAccentBrushKey}}" />
128+
<Setter TargetName="PART_ContentHost" Property="Visibility" Value="Visible"/>
129+
<Setter TargetName="PART_EllipsedTEXT" Property="Visibility" Value="Hidden"/>
114130
</Trigger>
115131
<MultiTrigger>
116132
<MultiTrigger.Conditions>
@@ -132,6 +148,7 @@
132148
<Setter Property="Foreground" Value="{DynamicResource {x:Static reskeys:ResourceKeys.ControlTextBrushKey}}"/>
133149
<Setter Property="Background" Value="{DynamicResource {x:Static reskeys:ResourceKeys.ControlInputBackgroundKey}}"/>
134150
<Setter Property="BorderThickness" Value="1" />
151+
<Setter Property="BorderBrush" Value="Transparent"/>
135152
<Setter Property="PopupBorderBrush" Value="{DynamicResource {x:Static reskeys:ResourceKeys.ControlInputBorderKey}}"/>
136153
<Setter Property="PopupBorderThickness" Value="1"/>
137154
<Setter Property="Padding" Value="0"/>

0 commit comments

Comments
 (0)