UWP應用載入SVG圖片的兼容性方案

新版本《紙書科學計算器》的更新點之一,就是優化了表達式的顯示方式。在舊版本中,表達式里的符號是用png圖片顯示的,當用戶放大看的時候會發現一些鋸齒,非常影響使用體驗。圖片的等比縮放也會導致符號的粗細不一,比較明顯的如下:

(矩陣的碩大括弧非常違和,細看還會有些鋸齒)

在開發新版本時,為了使表達式的顯示更精細,我打算用svg圖片來代替之前的png圖片,這種基於xml的矢量圖形格式既小巧又能有效避免鋸齒。雖然我也考慮過xaml里的path路徑,但是因為時間原因,我並不想大幅改動之前的代碼。如果uwp的Image控制項能像HTML5的img標籤一樣支持svg圖片那就太好了。那麼Image控制項到底滋不滋瓷svg呢?

答案很尷尬:首先你問我滋不滋瓷,我肯定是滋瓷的。但是只有在Windows 10 Creators Update (10.0.15063.0)之後才能直接支持。15063可是今年上半年才出的更新,毫無疑問還有大量用戶停留在14393甚至更低的版本。這裡不得不吐槽一下巨硬,svg的支持居然做得這麼晚,一向思維領先的uwp這次真的落後了很多。難道是為了推薦開發者一律用path路徑嗎?

由於要兼容14393及以下的版本,在這些版本的系統上只能使用第三方庫。經過搜索了解到,有個開源的矢量圖載入庫Mntone.SvgForXaml可以顯示svg圖片。經過實際測試,發現Mntone.SvgForXaml內部是parse了svg文件的xml再轉換成Bitmap繪製在Image控制項上實現的。雖然確實可以支持svg,但是顯示效果不佳:由於圖片經過了柵格化,用viewbox縮放後還是會有鋸齒。雖感無奈,但為了保證對低版本系統的支持也只能這樣了。

最後決定,14393及以下的版本的系統上使用Mntone.SvgForXaml,在15063以上的系統還是直接採用Image控制項載入svg,因為這個無論怎麼縮放都是無鋸齒的。

一、Mntone.SvgForXaml的使用及坑

關於Mntone.SvgForXaml的使用,網上已經有了很多帖子,比如UWP項目中載入svg矢量圖 - 菜鳥之路 - CSDN博客,基本的使用方法簡要介紹如下(大部分轉自該文章):

1.用NuGet包管理器在項目里添加Mntone.SvgForXaml,會自動添加依賴包Win2D.uwp;

2.在xaml文件中添加命名空間:

xmlns:svg="using:Mntone.SvgForXaml.UI.Xaml"n

3.在xaml文件中聲明該控制項:

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> n <svg:SvgImage x:Name="SvgControl"/>n</Grid> n

4.用C#代碼載入圖片(也可以在xaml中將SvgImage控制項的Source屬性用Bind綁定賦值):

public MainPage()n{n this.InitializeComponent();n this.Loaded += MainPage_Loaded;n}nnprivate async void MainPage_Loaded(object sender, RoutedEventArgs e)n{n var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/magnifier28.svg"));nn await this.SvgControl.LoadFileAsync(file);n}n

5.由於矢量圖都是載入到內存中,所以使用完後最好卸載一下:

protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)n{n this.SvgControl.SafeUnload();n}n

但是,在實際使用中,我發現這個庫是有坑的。SvgImage控制項如果是用C#動態添加到界面(在《紙書》里完全就是這樣的),在聲明控制項對象後不管是用LoadFileAsync方法,還是用它給的LoadSvg方法,還是為Source屬性賦值,在添加到界面後圖像都會消失不見。在Mntone.SvgForXaml的文檔中貌似也沒有什麼註明。摸索了半天,發現必須需要在控制項的Loaded事件里載入圖片才行……暈。具體方式將在下文補上。

二、直接支持svg的Image控制項

15063以上的系統可以在xaml中直接把svg圖片當成普通圖片為Image的Source屬性賦值。如:

<Image Source="example.svg" />n

在C#代碼中,可以用新的對象SvgImageSource(ImageSource的子類)為Image的Source屬性賦值。如:

image1.Source = new SvgImageSource(new Uri($"ms-appx:///{filePath}"));n

可見,SvgImageSource類的存在與否可以作為當前系統是否直接支持svg的標誌。所以靠ApiInformation類就可以輕易判斷了:

if(ApiInformation.IsTypePresent("Windows.UI.Xaml.Media.Imaging.SvgImageSource"))n{n image1.Source = new SvgImageSource(new Uri($"ms-appx:///{filePath}"));n}n

雖然uwp對svg支持很晚,但還是非常excited!

三、兩種方案的結合

在《紙書》中,我寫了一個靜態類SvgManager來全局掌控svg資源。由於《紙書》里用到的svg資源較少(運算符和函數不算多)但是會頻繁的載入,因此我在SvgManager里把所有svg資源都提到內存里來,以減少硬碟讀取次數,加快表達式的顯示速度。

大體上SvgManager類是這樣的:

/// <summary>n/// SVG資源管理類n/// </summary>npublic static class SvgManagern{n /// <summary>n /// 15063以上系統的svg資源n /// </summary>n private static Dictionary<string, SvgImageSource> svgSources;n /// <summary>n /// 14393以下系統的svg資源n /// </summary>n private static Dictionary<string, SvgDocument> svgDocuments;n /// <summary>n /// 是否可以直接使用Image控制項載入svgn /// </summary>n public static readonly bool CanDisplaySvgDirectly;nn //其他成員聲明省略n ...nn static SvgManager()n {n CanDisplaySvgDirectly = ApiInformation.IsTypePresent("Windows.UI.Xaml.Media.Imaging.SvgImageSource");n //讀取svg文件清單作為keyn if (CanDisplaySvgDirectly)n {n svgSources = new Dictionary<string, SvgImageSource>();n foreach (var n in svgFileNames)n svgSources.Add(n, null);n }n elsen {n svgDocuments = new Dictionary<string, SvgDocument>();n foreach (var n in svgFileNames)n svgDocuments.Add(n, null);n }n //一次性載入資源n loadResourcesAsync();n }nn /// <summary>n /// 載入SVG文檔n /// </summary>n private static async void loadResourcesAsync()n {n if (CanDisplaySvgDirectly)n {n foreach (var key in svgFileNames)n if (svgSources[key] == null)n svgSources[key] = getSource(key);n }n elsen {n foreach (var key in svgFileNames)n if (svgDocuments[key] == null)n svgDocuments[key] = await getDocumentAsync(key);n }n }nn private static SvgImageSource getSource(string fileName)n {n return new SvgImageSource(new Uri($"ms-appx:///Svg/{fileName}"));n }nn private static async Task<SvgDocument> getDocumentAsync(string fileName)n {n var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri($"ms-appx:///Svg/{fileName}"));n var stream = await file.OpenStreamForReadAsync();n var bytes = new byte[stream.Length];n await stream.ReadAsync(bytes, 0, bytes.Length);n //Mntone.SvgForXaml庫支持的parse操作n return SvgDocument.Parse(bytes);n }nn /// <summary>n /// 獲取Source對象n /// </summary>n /// <param name="fileName"></param>n /// <returns></returns>n public static SvgImageSource GetSource(string fileName)n {n if (svgSources.ContainsKey(fileName))n {n if (svgSources[fileName] == null)n svgSources[fileName] = getSource(fileName);n return svgSources[fileName];n }n elsen throw new Exception();n }nn /// <summary>n /// 在SvgImage控制項內顯示SVGn /// </summary>n /// <param name="si"></param>n public static async void LoadSvg(SvgImage si)n {n //初始化SvgImage時在Tag屬性里賦上了svg文件名n var filename = si.Tag.ToString();n if (svgDocuments.ContainsKey(filename))n {n if (svgDocuments[filename] == null)n svgDocuments[filename] = await getDocumentAsync(filename);n si.LoadSvg(svgDocuments[filename]);n }n elsen throw new Exception();n }n}n

之後,在創建svg圖片控制項的時候,先判斷一下SvgManager.CanDisplaySvgDirectly的值,如果為true則創建Image控制項,再用SvgManager.GetSource(filename)方法為Image的Source屬性賦值;如果為false,則創建第三方SvgImage控制項,然後把svg文件名賦在Tag屬性里,再添加Loaded事件的處理程序:

svgControl.Loaded += (sender, e) => SvgManager.LoadSvg(sender as SvgImage);n

這樣,在各大版本的Win10下都能愉快顯示svg了。

最終,新版本《紙書》的表達式顯示也得到了優化:

感覺一切都順暢多了~


推薦閱讀:

為何APP開發時都要開發者自己提供多種尺寸的圖標圖片?
現在是進入 Windows Universal Platform 開發的好時機嗎?
如何評價知乎UWP 2.0?
微軟uwp應用的三豎條結構合理嗎?
假如微軟搞一套Linux下的UWP運行環境並且開源,能否緩解WP手機缺乏應用的局面?

TAG:通用Windows平台UWP | Windows10 | UWPWindows开发 |