This article documents my experience in migrating a small WPF application to the UNO framework for support on the UOS(统信).
Before I begin, let me explain my requirements. I currently have a small WPF application that I need to run on both the UnionTech OS and Windows.
As we all know, there are many multi-platform development frameworks available in the current dotnet system. This time, I decided to try developing with the UNO/MAUI approach. The overall technical architecture is shown in the diagram below.
As shown in the diagram, I still use the WPF framework on Windows. However, this time the WPF framework is used as the underlying framework. Most of the business code will not directly touch the WPF framework, only some platform compatibility adaptation code will. The rest of the business code will indirectly use the WPF framework through the UNO and MAUI frameworks. On the UOS, the GTK application framework is used. Similarly, only platform compatibility adaptation code will touch the GTK application framework, and most business code will not directly interact with it.
The overall rendering layer uses SKIA to ensure consistent rendering effects across multiple platforms.
Daily Development
When creating a new project, remember to check the Windows project option. This will generate a WinUI3 project. When writing code, choose the WinUI 3 project to get XAML code intelligent prompts. When debugging, prioritize using the WinUI 3 project to debug the interface layout. You can directly use Visual Studio’s hot reload support for WinUI 3, which works better.
I recommend also adding the Skia.WPF and Skia.GTK projects. GTK can run on both Windows and Linux systems, but GTK may have some strange issues on Windows. In this case, switch to Skia.WPF. After all, those really released on the Windows platform won’t be so desperate to use GTK as the underlying layer.
Text
Flickering Black Screen on UOS
This is an OpenGL issue. For the fix, please see https://github.com/unoplatform/uno/issues/13530.
Chinese Text Garbled
Chinese text garbled is due to the incorrect loading of Chinese fonts. This problem also applies to languages such as Korean or Japanese. UOS has the Source Han Sans font by default, and GTK will automatically roll back the font. All you need to do is set the application to use Microsoft YaHei. Setting it to Microsoft YaHei will allow the application to display normal sans-serif fonts on both Windows and UOS systems.
The setting method is as follows:
<TextBlock Text="解决 UOS 中文乱码" FontFamily="Microsoft YaHei UI"/>
Remember to use the Microsoft YaHei UI
font on the interface, the font with UI
. Otherwise, you will see some strange font layouts.
Addition: Uno with Wpf Chinese code display messy code · Issue #6973 · unoplatform/uno
TextBox Stretching Space
If there is content that depends on the space stretched by the measurement during the TextBox input process, then the stretched space may be incorrect, such as the following code:
<TextBox HorizontalAlignment="Center" FontSize="50"></TextBox>
With this logic, you will see the text content being clipped during the input process. Basically, you can see the text content being clipped under the Skia.WPF and Skia.GTK projects.
For now, the only workaround is to modify the interface design.
TextBox’s Minimum Height
The minimum height will still be higher than expected, so you can only modify the interface design to work around it.
TextBox’s Scroll Bar
For example, to scroll to the bottom, you can use the following code:
private void ScrollToBottom(TextBox textBox)
{
//textBox.Spy();
if(textBox.VisualDescendant<ScrollViewer>() is { } scrollViewer)
{
scrollViewer.ChangeView(0.0f, scrollViewer.ExtentHeight, 1.0f, true);
}
}
This VisualDescendant method is an auxiliary method, the code is as follows:
static class TreeExtensions
{
public static T? VisualDescendant<T>(this UIElement element) where T : DependencyObject
=> VisualDescendant<T>((DependencyObject) element);
public static T? VisualDescendant<T>(DependencyObject element) where T : DependencyObject
{
if (element is T)
{
return (T) element;
}
T? foundElement = default;
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
{
var child = VisualTreeHelper.GetChild(element, i);
foundElement = VisualDescendant<T>(child);
if (foundElement != null)
{
break;
}
}
return foundElement;
}
}
This method is also useful for ListView and others. The core is to find the ScrollViewer object through the visual tree and control the scrolling through the ScrollViewer.
StreamGeometry Resources for Geometric Shapes
In WPF, icons often use Path geometric paths as vector icons, which are put into StreamGeometry resources. StreamGeometry resources made from a single Path can be replaced in UNO with x:String
, as the following code shows an icon originally placed in WPF resources:
<StreamGeometry x:Key="Geometry.Close">
M18.363961,5.63603897 C18.7544853,6.02656326 18.7544853,6.65972824 18.363961,7.05025253 L13.4142136,12 L18.363961,16.9497475 C18.7544853,17.3402718 18.7544853,17.9734367 18.363961,18.363961 C17.9734367,18.7544853 17.3402718,18.7544853 16.9497475,18.363961 L12,13.4142136 L7.05025253,18.363961 C6.65972824,18.7544853 6.02656326,18.7544853 5.63603897,18.363961 C5.24551468,17.9734367 5.24551468,17.3402718 5.63603897,16.9497475 L10.5857864,12 L5.63603897,7.05025253 C5.24551468,6.65972824 5.24551468,6.02656326 5.63603897,5.63603897 C6.02656326,5.24551468 6.65972824,5.24551468 7.05025253,5.63603897 L12,10.5857864 L16.9497475,5.63603897 C17.3402718,5.24551468 17.9734367,5.24551468 18.363961,5.63603897 Z
</StreamGeometry>
In WPF, suppose it is set on a button as an icon button, you can define a style, the content is roughly as follows:
<Style x:Key="Style.TitlebarButton" TargetType="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="#808080" />
<Setter Property="Width" Value="24"/>
<Setter Property="Height" Value="24"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ButtonBase}">
<Grid Background="{TemplateBinding Background}" UseLayoutRounding="True">
<Path Fill="{TemplateBinding Foreground}"
Data="{Binding Path=Content, RelativeSource={RelativeSource TemplatedParent}}">
</Path>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
The code used in the specific business is roughly as follows:
<Button Style="{StaticResource Style.TitlebarButton}" Content="{StaticResource Geometry.Close}"/>
After moving to UNO, change the StreamGeometry type resource to an x:String
resource, as in the following code:
<x:String x:Key="Geometry.Close">M18.363961,5.63603897 C18.7544853,6.02656326 18.7544853,6.65972824 18.363961,7.05025253 L13.4142136,12 L18.363961,16.9497475 C18.7544853,17.3402718 18.7544853,17.9734367 18.363961,18.363961 C17.9734367,18.7544853 17.3402718,18.7544853 16.9497475,18.363961 L12,13.4142136 L7.05025253,18.363961 C6.65972824,18.7544853 6.02656326,18.7544853 5.63603897,18.363961 C5.24551468,17.9734367 5.24551468,17.3402718 5.63603897,16.9497475 L10.5857864,12 L5.63603897,7.05025253 C5.24551468,6.65972824 5.24551468,6.02656326 5.63603897,5.63603897 C6.02656326,5.24551468 6.65972824,5.24551468 7.05025253,5.63603897 L12,10.5857864 L16.9497475,5.63603897 C17.3402718,5.24551468 17.9734367,5.24551468 18.363961,5.63603897 Z</x:String>
The rest of the code is basically the same as WPF, as in the following UNO button style
<Style x:Key="Style.TitlebarButton" TargetType="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="#808080" />
<Setter Property="Width" Value="24"/>
<Setter Property="Height" Value="24"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ButtonBase">
<Grid Background="{TemplateBinding Background}">
<Path Fill="{TemplateBinding Foreground}" Data="{TemplateBinding Content}"></Path>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Then you can see the button code define is same as the WPF code:
<Button Style="{StaticResource Style.TitlebarButton}" Content="{StaticResource Geometry.Close}"/>
PathGeometry
Some parts are not supported, so multi-platform testing is required. You may need to find a workaround.
x:Static
Static binding is not supported, so a workaround is necessary. For example, you can redefine an instance property that references the static value, and then bind to the instance property.
Alternatively, you can move some static properties to the resource dictionary.
For instance, in WPF, you would write it like this:
public static class BooleanToVisibility
{
public static IValueConverter CollapsedWhenTrue { get; private set; } = new VisibilityConverter
{
Visible = false,
Collapsed = true
};
}
<Border Visibility="{Binding Foo, Converter={x:Static uiConverters:BooleanToVisibility.CollapsedWhenTrue}}"/>
In UNO, you would modify it to use the resource dictionary:
<UserControl.Resources>
<uiConverters:VisibilityConverter x:Key="CollapsedWhenTrue" Visible="False" Collapsed="True"/>
</UserControl.Resources>
<Border Visibility="{Binding Foo, Converter={StaticResource CollapsedWhenTrue}}">
Image Resources
Image resources can use relative or absolute paths. The format for absolute paths in UNO is as follows:
<Image Source="ms-appx:///[MyApp]/Assets/MyImage.png" />
The [MyApp]
in the above code is optional, but I recommend including it. This [MyApp]
corresponds to the assembly name.
By default, all images are referenced as Content
. You can see the following code in the csproj project file:
<Content Include="Assets\**;**/*.png;**/*.bmp;**/*.jpg;**/*.dds;**/*.tif;**/*.tga;**/*.gif" Exclude="bin\**;obj\**;**\*.svg" />
Newly added image files do not require any modifications by default. However, for platform compatibility, I recommend using png, jpg, and bmp formats, as all platforms support these formats. If your image does not display, please follow these steps:
- Check if you have modified the csproj and inadvertently ignored your image.
- Try using an absolute path for the resource.
- Compare the absolute path character by character to ensure it is correct.
- Verify that the path starts with the string
ms-appx:///
. You need to use three/
characters. - If you still can’t see the image, try regenerating it.
- If it still doesn’t work, check if the image format is unusual, such as changing the webp image extension to png, etc.
Images can be used as content in the resource dictionary. You can use the BitmapImage type, which is the same as in WPF. However, the content of the Source needs to be changed under the absolute path, as shown in the following example:
<BitmapImage x:Key="Image.Logo.Size24" UriSource="ms-appx:///[MyApp]/Assets/Logo/logo24x24.png"></BitmapImage>
For more information, please refer to the official documentation Assets and image display
ContentControl
The functionality aligns with WPF, but the default style behavior is different. The default HorizontalContentAlignment and VerticalContentAlignment are in the top left corner. You need to set them to Stretch to align with WPF.
<ContentControl HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"></ContentControl>
Default Control Properties
Most control default properties are the same as in WPF. However, a few layout properties are different. For example, many controls’ HorizontalAlignment and VerticalAlignment are in the top left corner. You need to set them to Stretch to align with WPF.
Changes to csproj
Due to some conflicts between UNO and VisualStudio, creating a new file may cause UNO’s csproj to add unnecessary code. During the development process, before uploading to git, check whether the changes to csproj are necessary. If the changes are unnecessary, please revert them. Generally, you need to revert the changes to csproj after creating a new file, such as creating a new type or user control.
Dispatcher
The Dispatcher in UNO is weaker than that in WPF, but some replacements can be made. The logic of obtaining the Dispatcher from the original interface elements remains unchanged.
The logic of obtaining it statically, such as the following WPF code, needs to be replaced.
System.Windows.Application.Current.Dispatcher.InvokeAsync
The method of obtaining the static main thread dispatcher from UNO is the same as that of UWP or WinUI 3, as shown in the following code.
await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
// Write the dispatch implementation code here
});
Unlike WPF’s Dispatcher scheduling level, UNO’s schedulable level is very limited, with only the following schedulable levels.
High 1 High priority. Delegates for all synchronous requests are called immediately. Asynchronous requests are queued and processed before any other type of request.
Idle -2 Lowest priority. Use this priority for background tasks. Delegates are processed when the main thread of the window is idle and there are no pending inputs in the queue.
Low -1 Low priority. Delegates are processed if there are no higher priority events pending in the queue.
Normal 0 Normal priority. Delegates are processed in the order scheduled.
In most cases, the Normal priority is used.
However, when running WinUI 3, the acquisition of the CoreApplication.MainView.CoreWindow
property may throw an exception that it cannot be created repeatedly. If you try to get the CoreApplicationView object indirectly from CoreApplication.GetCurrentView()
to get the Dispatcher, it may still fail, because this method will throw a System.Runtime.InteropServices.COMException: “Element not found” exception.
A more secure way is to store the Microsoft.UI.Dispatching.DispatcherQueue in the App yourself, so that you can get the same DispatcherQueue object obtained from the main UI thread and use it on WinUI 3, WPF and GTK projects. In the WinUI 3 project, the MainWindow.Dispatcher property is still null, which is why the DispatcherQueue is used.
public class App : EmbeddingApplication
{
protected async override void OnLaunched(LaunchActivatedEventArgs args)
{
// 忽略其他代码
MainWindow = builder.Window;
#if DEBUG
MainWindow.EnableHotReload();
#endif
Dispatcher = MainWindow.DispatcherQueue;
Host = await builder.NavigateAsync<Shell>();
}
public Microsoft.UI.Dispatching.DispatcherQueue Dispatcher { private set; get; } = null!;
}
Missing Mechanisms
Visibility.Hidden
There is no hidden option, instead, you can set the opacity to 0. Setting Opacity="0"
has a similar effect to WPF’s Visibility.Hidden
.
MultiBinding
MultiBinding is not supported, so you have to find a workaround and write the interface with only single binding.
ControlTemplate.Triggers
This is not supported, a workaround is needed.
Use of x:Name attribute in Resources
The use of x:Name in resources is not supported. This is because x:Name must be assigned a property or field when it is generated, but resources can be created multiple times, which the generated code cannot handle. This issue was previously raised by Avalonia’s XAML creator, and now WinUI 3, UNO, and MAUI all have this problem.
The simplest reproduction code is as follows:
<Page.Resources>
<ResourceDictionary>
<SolidColorBrush x:Name="MyBrush" Color="Blue"/>
</ResourceDictionary>
</Page.Resources>
In this case, you should use x:Key
instead of x:Name
to meet the expectation.
In addition, for the binding logic within resources, such as the following code, you can only find a workaround for this kind of code:
<Page.Resources>
<ControlTemplate x:Key="Template.Loading" TargetType="ContentControl">
<Grid x:Name="RootGrid" />
</ControlTemplate>
</Page.Resources>
The error message for the above code is error CS0103: The name "_RootGrid" does not exist in the current context
.
For more information, please see Adding Name to a Resource fails on build · Issue #1427 · unoplatform/uno.
IPC
Known issue: https://github.com/dotnet-campus/dotnetCampus.Ipc/issues/139
References
【Chinese Blog】 WPF uses MAUI’s custom drawing logic
I share a video of an app which created with uno · unoplatform/uno · Discussion #4962
本文会经常更新,请阅读原文: https://blog.lindexi.com/post/Notes-on-Migrating-from-WPF-to-UNO-under-UOS.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
如果你想持续阅读我的最新博客,请点击 RSS 订阅,推荐使用RSS Stalker订阅博客,或者收藏我的博客导航
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 。
无盈利,不卖课,做纯粹的技术博客
以下是广告时间
推荐关注 Edi.Wang 的公众号
欢迎进入 Eleven 老师组建的 .NET 社区
以上广告全是友情推广,无盈利