-
Notifications
You must be signed in to change notification settings - Fork 770
Description
WinUI has delivered innovative and impressive new GUI elements, designs, and techniques, and the WinUI team has implemented many clever improvements. But of course, nobody is capable of 100% pure perfection, so some deficiences must exist in WinUI. So now I'd like to talk about a glaring exception in the impressive track record of WinUI; an area desperately in need of improvement: Icon rendering in WinUI hardly works -- it's a disaster currently.
Icons are a very important topic, considering that most apps uses multiple icons in multiple places in multiple kinds of GUI elements (icons in buttons, toolbars, menus, tabs, standalone, etc). Thus it's really surprising to observe that, currently, icon rendering hardly works in WinUI/UWP unfortunately. Considering the extreme ubiquity of icons in apps, I consider this to be a high priority topic.
Use a PNG image as an icon?
For starters, look at a really simple case where you have an instance of Windows.UI.Xaml.Media.Imaging.BitmapImage
that was produced from a PNG, and you want to use this PNG BitmapImage
as an icon. This should be super simple, right? But no, unfortunately neither Windows.UI.Xaml.Controls.BitmapIconSource
nor Windows.UI.Xaml.Controls.BitmapIcon
support the ability to render a specified instance of BitmapImage
-- surprising but true.
To use your BitmapImage
instance as an icon, you are forced to use Windows.UI.Xaml.Controls.Image
to render it because BitmapIconSource
cannot. This means that, in combination with the other problems I will describe, Windows.UI.Xaml.Controls.IconSourceElement
is a nice idea but so badly broken that it's practically unusable currently.
See also: Proposal: Create IconSource from Bitmap stream #601
Use an SVG image as an icon?
For example, you have an SVG image file that you want to use as an icon in multiple places in your app. You're out of luck because the SVG renderer contains bugs. See the most recent messages in this issue:
SVG Image sources behave unpredictably. #825
The names of BitmapIconSource
and BitmapIcon
suggest that they will not render SVG images because SVG images are vector not bitmap. I haven't actually tested what happens if you try to set BitmapIconSource.UriSource
to the URI of an SVG instead of a bitmap. Unfortunately an IconSource
subclass named ImageIconSource
does not exist currently.
I am surprised to observe that WinUI does not yet have good support for scalable vector icons, neither colored vector icons (such as SVG) nor monochrome vector icons/symbols (such as paths).
SymbolIconSource and SymbolIcon are often unusable
For example, imagine you want to create a button that looks similar to this:
Thus you write XAML like this:
<Button Margin="20">
<StackPanel Orientation="Vertical">
<IconSourceElement Width="64" Height="64" Margin="8">
<IconSourceElement.IconSource>
<SymbolIconSource Symbol="Refresh"/>
</IconSourceElement.IconSource>
</IconSourceElement>
<TextBlock Text="Reload Data" FontSize="20" FontWeight="SemiBold"/>
</StackPanel>
</Button>
But it fails because the above XAML renders as follows:
As you can see, regardless of how large or small you make your IconSourceElement
, the symbol still renders at a constant size of 20! Yes, a constant size! I could hardly believe it. Shockingly, SymbolIconSource
and SymbolIcon
don't scale, and don't provide any way to manually configure the size of the icon, and don't have a FontSize
property either. (A FontSize
property wouldn't be the right solution but anyway it doesn't exist.)
Following is an awkward inconvenient inefficient workaround that uses a Viewbox
element to hack the icon into scaling. An alternative workaround is to view SymbolIconSource
as unusable, and stop using it entirely, and instead use a TextBlock
element and set its TextBlock.FontSize
property.
<Button Margin="20">
<StackPanel Orientation="Vertical">
<Viewbox Width="64" Height="64" Margin="8">
<IconSourceElement>
<IconSourceElement.IconSource>
<SymbolIconSource Symbol="Refresh"/>
</IconSourceElement.IconSource>
</IconSourceElement>
</Viewbox>
<TextBlock Text="Reload Data" FontSize="20" FontWeight="SemiBold"/>
</StackPanel>
</Button>
How IconSourceElement
with SymbolIconSource
should behave: The same as BitmapIconSource
and Windows.UI.Xaml.Controls.Image
-- both of those scale the icon/image whereas SymbolIconSource
does not -- a strange inconsistency. The icon should be scaled in the same manner as an Windows.UI.Xaml.Controls.Image
element does with its default setting of Stretch="Uniform"
, or the same as a Windows.UI.Xaml.Shapes.Path
element with Stretch
set to Uniform
.
FontIconSource and FontIcon are also broken
FontIconSource
and FontIcon
are slightly less broken than SymbolIconSource
because they have a FontSize
property. Actually a FontSize
property is a bad solution but at least there exists some way of changing the icon size, and a bad way of changing the icon size is better (less bad) than the nothing that SymbolIconSource
provides.
The idea of applying a FontSize
to an icon is strange and doesn't make sense. What makes sense is applying a Width and Height to an icon or image or path, not a FontSize. A FontSize property is applicable to text but an icon is not text, rather an icon is akin to an image or path.
Instead of trying to apply font size to an icon, SymbolIconSource
and FontIconSource
should scale the icon in the same manner as BitmapIconSource
does.
SymbolIconSource
handles white space better than FontIconSource
. For example, have a look at how this SymbolIconSource
snippet renders:
<Border Padding="0" HorizontalAlignment="Left" VerticalAlignment="Top" BorderBrush="CadetBlue" BorderThickness="2">
<IconSourceElement Margin="0">
<IconSourceElement.IconSource>
<SymbolIconSource Symbol="Refresh"/>
</IconSourceElement.IconSource>
</IconSourceElement>
</Border>
The above rendering makes sense. The Border
encloses the icon without padding. Now compare it with the failure of FontIconSource
and FontIcon
:
<Border Padding="0" HorizontalAlignment="Left" VerticalAlignment="Top" BorderBrush="CadetBlue" BorderThickness="2">
<IconSourceElement Margin="0">
<IconSourceElement.IconSource>
<FontIconSource Glyph="↻" FontFamily="Global User Interface" />
</IconSourceElement.IconSource>
</IconSourceElement>
</Border>
As you can see above, FontIconSource
and FontIcon
render with padding/whitespace around the icon. In practice, this makes it difficult to use the icon in a button because the spacing/layout looks strange because of the uncontrollable extra space introduced by FontIconSource
. The amount of white space on the left, top, right, and bottom sides varies depending on the font -- the white space comes from the font.
If you have several IconSourceElement
instances, such as 10 different buttons (in a row) with icons, and some of those IconSourceElement
instances use SymbolIconSource
and some use FontIconSource
and some use PathIconSource
, then the 10 buttons look strange and inconsistent with each other because some have extra whitespace/padding (those that use FontIconSource
) while the others don't.
FontIconSource
and FontIcon
are not any better than using a TextBlock
element. They render the same as TextBlock
. This rendering behavior makes no sense because they are supposed to render as icons not text.
If you use TextBlock
instead of FontIcon
, you might try to use the OpticalMarginAlignment
property to eliminate the problematic whitespace/padding, but it doesn't work, as you can see following. The first image (left) is with OpticalMarginAlignment="None"
and the second image (right) is with OpticalMarginAlignment="TrimSideBearings"
. As you can see, TrimSideBearings didn't trim anything at all, rather it moved the space on the left side to the right side (the total width remains unchanged), and it didn't trim the top and bottom sides, and no other trimming option exists in OpticalMarginAlignment
.
<StackPanel Orientation="Horizontal">
<Border Margin="5" Padding="0" HorizontalAlignment="Left" VerticalAlignment="Top" BorderBrush="CadetBlue" BorderThickness="2">
<TextBlock Text="⎈" FontFamily="Global User Interface" FontSize="100" OpticalMarginAlignment="None" Margin="0" Padding="0"/>
</Border>
<Border Margin="5" Padding="0" HorizontalAlignment="Left" VerticalAlignment="Top" BorderBrush="CadetBlue" BorderThickness="2">
<TextBlock Text="⎈" FontFamily="Global User Interface" FontSize="100" OpticalMarginAlignment="TrimSideBearings" Margin="0" Padding="0"/>
</Border>
</StackPanel>
In WPF, one way of rendering a glyph/character as an icon (without whitespace/padding) is to do this:
- Use either
System.Windows.Media.FormattedText.BuildGeometry
orSystem.Windows.Media.GlyphRun.BuildGeometry
to convert the glyph/character to aGeometry
instance. - Create an instance of
System.Windows.Shapes.Path
and set thePath.Data
property to theGeometry
returned byBuildGeometry
. - Set the
Path.Stretch
property toUniform
.
Sadly, this technique cannot be used in WinUI because an equivalent of FormattedText.BuildGeometry
does not exist in WinUI (or not that I'm aware of).
PathIconSource and PathIcon are unusable
For example, you have an instance of Windows.UI.Xaml.Media.PathGeometry
that you want to use as a monochrome vector icon in multiple buttons, pages, or places in your app. This should be super simple, right? Just use Windows.UI.Xaml.Controls.PathIconSource
or Windows.UI.Xaml.Controls.PathIcon
and give it whatever PathGeometry
instance you want? But no, unfortunately this fails to render. Surprising but true.
Following you can see the success of Windows.UI.Xaml.Shapes.Path
versus the failure of Windows.UI.Xaml.Controls.PathIconSource
(or PathIcon
), when rendering the same PathGeometry
(a house symbol). The first image (left) is the Path
element -- it works. The second image (right) is PathIconSource
-- a failed (truncated) rendering of an icon.
<StackPanel Orientation="Horizontal">
<Border Margin="5" Padding="0" HorizontalAlignment="Left" VerticalAlignment="Top" BorderBrush="CadetBlue" BorderThickness="2">
<Path Width="90" Height="90" Stretch="Uniform" Fill="Black">
<Path.Data>
M81.799,0 L163.599,75.636001 146.368,75.636001 146.368,136.341 17.229999,136.341 17.229999,75.636001 0,75.636001 23.987998,53.455336 23.987998,16.938999 53.620998,16.938999 53.620998,26.054981 z
</Path.Data>
</Path>
</Border>
<Border Margin="5" Padding="0" HorizontalAlignment="Left" VerticalAlignment="Top" BorderBrush="CadetBlue" BorderThickness="2">
<IconSourceElement Width="90" Height="90">
<IconSourceElement.IconSource>
<PathIconSource>
<PathIconSource.Data>
M81.799,0 L163.599,75.636001 146.368,75.636001 146.368,136.341 17.229999,136.341 17.229999,75.636001 0,75.636001 23.987998,53.455336 23.987998,16.938999 53.620998,16.938999 53.620998,26.054981 z
</PathIconSource.Data>
</PathIconSource>
</IconSourceElement.IconSource>
</IconSourceElement>
</Border>
</StackPanel>
The Path
element succeeds because I set its Stretch
property to Uniform
, but this solution does not work for PathIconSource
nor PathIcon
-- they have no Stretch
property. Thus PathIconSource
and PathIcon
are unusable and you're forced to use the Path
element instead, which means that IconSourceElement
is a nice idea but so badly broken that it's practically unusable currently.
The fact that PathIconSource
and PathIcon
don't scale the icon is especially strange when you consider that BitmapIconSource
does scale the icon.
A second reason why PathIconSource
and PathIcon
are broken is that if you try to use the same icon in multiple buttons, pages, or places in your app, it fails (throws an exception). Unfortunately the Path
element also suffers from the same failure, but it only fails in WinUI, not in WPF. In WPF, you can "freeze" a Geometry
instance and then use it in an unlimited number of places in your app. For more info, see this issue:
PathIconSource claims to support sharing but throws exception (AKA shared Geometry issue) #827
If your PathGeometry
instance represents a stroked figure instead of a filled figure, such as a "+" symbol consisting of a horizontal stroked lines over a vertical stroked line, then PathIconSource
does not support your PathGeometry
. See issue:
PathIcon/PathIconSource should allow stroke to be set as well as fill #1279
Library of icons in app (re ResourceDictionary)
Edited:
I remembered another important part of this issue. When using PathIconSource
, obviously every app needs to store a collection of all the icons that the app uses (icons as paths/geometries in the case of PathIconSource
). This should be super simple to do because WinUI already supports the excellent ResourceDictionary
feature. Therefore, our problem is simply solved by storing the app's icons in a "MyResourceDictionary.xaml" file, right? But no, unfortunately this only works in WPF, whereas it is still broken in WinUI, even after the number of years that have elapsed since the release of UWP/WinUI.
In WPF, you can easily store a collection of icons in your app like this:
<ResourceDictionary xmlns="..." xmlns:x="...">
<PathGeometry x:Key="HouseSymbol">
M81.799,0 L163.599,75.636001 146.368,75.636001 146.368,136.341 17.229999,136.341 17.229999,75.636001 0,75.636001 23.987998,53.455336 23.987998,16.938999 53.620998,16.938999 53.620998,26.054981 z
</PathGeometry>
<PathGeometry x:Key="RefreshSymbol">
...
</PathGeometry>
<PathGeometry x:Key="SettingsSymbol">
...
</PathGeometry>
</ResourceDictionary>
Actually, to be precise, in WPF, it's recommended to optimize the above by simply replacing PathGeometry
with the optimized read-only equivalent of PathGeometry
that is named StreamGeometry
. Easily done:
<StreamGeometry x:Key="HouseSymbol">
M81.799,0 L163.599,75.636001 146.368,75.636001 146.368,136.341 17.229999,136.341 17.229999,75.636001 0,75.636001 23.987998,53.455336 23.987998,16.938999 53.620998,16.938999 53.620998,26.054981 z
</StreamGeometry>
So that works great in WPF, but StreamGeometry
is unsupported in WinUI. As for the other class, PathGeometry
, it does exist in WinUI but it still produces a compile-time error message when you try to use it (PathGeometry
) in a ResourceDictionary XAML file:
Error XLS0503
A value of type 'String' cannot be added to a collection or dictionary of type 'PathFigureCollection'.
This bug could be easily fixed AFAIK, because most of the necessary work is already done, therefore I find it surprising that this bug still hasn't been prioritized after this number of years. I thought it was standard recommended practice to increase the priority of a bugfix when the bug is easy to fix.
AFAIK the PathGeometry
bug is easy to fix because the WinUI XAML compiler already compiles the following successfully, therefore most of the work is already done for PathGeometry
.
<ResourceDictionary xmlns="..." xmlns:x="...">
<Path x:Key="HousePathElement">
<Path.Data>
M81.799,0 L163.599,75.636001 146.368,75.636001 146.368,136.341 17.229999,136.341 17.229999,75.636001 0,75.636001 23.987998,53.455336 23.987998,16.938999 53.620998,16.938999 53.620998,26.054981 z
</Path.Data>
</Path>
</ResourceDictionary>
Although Path
in ResourceDictionary
compiles successfully, you should use PathGeometry
not Path
. Path
is derived from Windows.UI.Xaml.FrameworkElement
and Windows.UI.Xaml.UIElement
, and each FrameworkElement
instance is permitted to have only one parent FrameworkElement
instance, and this won't change. Thus you cannot (and should not) try to share the same Path
instance among multiple different buttons in your app where the same icon needs to be used in multiple places.
Unlike the Path
element, PathGeometry
and/or StreamGeometry
are suitable for being shared/used in multiple places, but as I mentioned in issue #827, it only works in WPF. Currently WinUI malfunctions (throws an exception) when you try to share/use the same PathGeometry
or PathIconSource
instance in multiple places, even despite the fact that the documentation says that sharing is supported:
"PathIconSource is similar to PathIcon. However, because it is not a FrameworkElement, it can be shared."
Notice how the documentation says "because it is not a FrameworkElement". This is the same reason why the ResourceDictionary
should contain PathGeometry
not Path
. Path
derives from FrameworkElement
whereas PathGeometry
does not.
The conclusion is that WPF (but not yet WinUI) supports the storage of the app's icons in a "MyResourceDictionary.xaml" file. This is very strange when you consider the fact that the only thing stopping WinUI from supporting it is a small easily-fixed bug in the XAML compiler. (Or maybe the bugfix is more difficult to implement than it appears, because maybe some reason exists that I don't know about, but anyway, this bug needs to be fixed.)
Apps definitely need this ability to store their own icons/symbols, instead of being limited to predefined icons from Microsoft. SymbolIconSource
is an excellent feature (thanks!) but obviously it's unreasonable to limit apps to using only the predefined icons/symbols in the Windows.UI.Xaml.Controls.Symbol
enumeration.
Classes were not updated for IconSource
When the IconSourceElement
and IconSource
classes was created in WinUI in order to replace the defective IconElement
subclasses, unfortunately various other classes were not updated accordingly. They should have been updated at the same time as when IconSource
was released. For example, in order to update MenuFlyoutItem
, this needed to be done:
- Create a property in
MenuFlyoutItem
namedIconSource
of typeIconSource
. - Mark the old property
MenuFlyoutItem.Icon
as obsolete by applyingSystem.ObsoleteAttribute
. (This old property usesWindows.UI.Xaml.Controls.IconElement
.)
public class MenuFlyoutItem : ...
{
public Windows.UI.Xaml.Controls.IconSource IconSource { get; set; }
[System.ObsoleteAttribute("Use IconSource property instead of Icon property.")]
public Windows.UI.Xaml.Controls.IconElement Icon { get; set; }
}
The following have not yet been updated:
Windows.UI.Xaml.Controls.AppBarButton.Icon
Windows.UI.Xaml.Controls.AppBarToggleButton.Icon
Windows.UI.Xaml.Controls.MenuFlyoutItem.Icon
Windows.UI.Xaml.Controls.MenuFlyoutSubItem.Icon
Windows.UI.Xaml.Controls.NavigationViewItem.Icon
Windows.UI.Xaml.Controls.Primitives.NavigationViewItemPresenter.Icon
Windows.UI.Xaml.Controls.SettingsFlyout.IconSource
(usesImageSource
notIconSource
)
Already up-to-date:
Windows.UI.Xaml.Controls.SwipeItem.IconSource
Windows.UI.Xaml.Input.XamlUICommand.IconSource
Although the subclasses of IconSource
currently render in a broken manner, this will be fixed and IconSource
is still a good idea, thus the various other classes need to be updated to support IconSource
instead of the old IconElement
subclasses.
Conclusion: They're all broken!
Windows.UI.Xaml.Controls.IconSourceElement
is a nice idea and its concept makes good sense, but unfortunately all subclasses of IconSource
are currently broken:
BitmapIconSource
SymbolIconSource
FontIconSource
PathIconSource
Fixing this iconic disaster should be a high priority because icons are very common in apps. Despite the fact that nearly all apps use icons, icon rendering hardly works in WinUI!