• 分享下最近基于Avalonia UI和MAUI写跨平台时间管理工具的体验


    起因

    几个月前,我在寻找一款时间管理软件,类似番茄时钟的工具,但是希望可以自定义时间。

    需要自定义的场景

    1. 做雅思阅读,3篇文件需要严格控制时间分配,需要一个灵活的计时器
    2. 定期提醒,每30分钟需要喝水或者上个厕所或者摸一下鱼...

    总结起来就是:专注一段时间,比如30分钟,然后休息10分钟,且没有杂七杂八的功能。
    理论上有的番茄时钟也能满足需求,但是我的需求是:

    • 界面尽可能的简洁。
    • 免费使用且最好是开源的。
    • 可以自定义时间。
    • 最好能跨平台,因为有时候是在macOS下使用,有时候又是在Windows上。
      但就其中部份条件还好,完全符合的竟然没符合我需求的。

    在Apple store找到一个比较接近需求的一款,叫iTimer, 非常简洁好用,但是自定义时间需要内购,且只能在macOS下。

    于是我在使用的时候就想,这软件功能极简,就几个页面,为什么我不自己做一个能。 于是每次利用一点时间空隙我就写一部份,一开始是选型MAUI,然后中途切换成Avalonia,最后基本完成了这个简易的版本。这里记录下开发心得
    结论是:
    代码都是C# + XAML,没有很复杂的逻辑和代码,新手完全可以轻松写一个日常使用的UI Tool。

    代码放在Github,也没啥技术含量,有需要的自取
    https://github.com/hoyho/iTimeSlot/tree/main

    暂时没有发布二进制文件
    需要的自己用git 克隆下来,然后dotnet build 或者dotnet publish即可

    成品预览

    macOS下使用默认主题:

    使用Material Theme

    Windows和Linux (使用xfce 桌面)

    其他杂七杂八的需求
    弹窗, 托盘等

    就目前而言,基本能满足我的需求了。

    谈谈体验

    why choose MAUI

    一开始,觉得是微软官方出的框架,应该不会有啥大坑吧,于是看了下官方介绍,文档的demo

    • 可以iOS, Android,macOS, Windows, Looks good
    • 不同平台的UI实现不一样,比如在Windows上是WinUI,在macOS上则是Mac Catalyst, 即UIKit, AppKit平台开放的API等等, 看起来还挺好看的😶
    • 文档也很清晰,至少比avalonia的清晰
      就哼哧哼哧地把环境配置,然后写了个Hello world.
      也就是这个

    我是在macOS开发的,按照文档来就好,
    https://learn.microsoft.com/en-us/dotnet/maui/get-started/installation?view=net-maui-8.0&tabs=visual-studio-code

    相比Windows下的Visual studio,使用vs code来开发而且还要
    macOS 的开发套件
    xcode-select --install
    中间错了个错误,具体什么错误忘记了,后来加上sudo执行就OK了

    持续踩坑

    组件picker 在macOS下没有默认值,需要点击后才能正常显示

    App设置倒计时需要设置一个时间段,于是选择了人picker组件,然而测试下来,在macOS下运行时,即使绑定了一组数据后,
    组件默认是没有选择上的,而是点击了done按钮后才能正常选择,具体同 https://github.com/dotnet/maui/issues/10208
    显然是一个bug。。。

    后来解决办法: 窗体初始化的时候主动设置一个SelectedIndex来触发变更,从而绑定上数据源

    macOS 上有办法实现关闭窗口后不退出

    本来期望是设置了之后,点击关闭按钮能Hook住关闭事件,然后继续后台运行,这在传统的WinForm或者 GTK框架都能轻松实现
    然而MAUI的设计似乎更倾向于移动端也就iOS和Android的生命周期,点击即关闭。

    好吧,也不是不能用.

    无法实现托盘后台运行

    还是macOS下,暂时也没找到原生的方式实现托盘后台运行,并支持右键菜单
    经过一番挣扎,找到了官方的一个demo有类似的实现。
    但是不是原生支持,而是通过调用object-c语言绑定,通过动态链接库来调用macOS提供接口objc_msgSend,然后访问系统提供的接口来实现比如这个NSStatusBar
    这是一个完整的在macOS下,TrayServic实现,来欣赏下:

    using System.Runtime.InteropServices;
    using Foundation;
    using ObjCRuntime;
    using WeatherTwentyOne.Services;
    
    namespace WeatherTwentyOne.MacCatalyst;
    
    public class TrayService : NSObject, ITrayService
    {
        [DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")]
        public static extern IntPtr IntPtr_objc_msgSend_nfloat(IntPtr receiver, IntPtr selector, nfloat arg1);
    
        [DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")]
        public static extern IntPtr IntPtr_objc_msgSend_IntPtr(IntPtr receiver, IntPtr selector, IntPtr arg1);
    
        [DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")]
        public static extern IntPtr IntPtr_objc_msgSend(IntPtr receiver, IntPtr selector);
    
        [DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")]
        public static extern void void_objc_msgSend_IntPtr(IntPtr receiver, IntPtr selector, IntPtr arg1);
    
        [DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")]
        public static extern void void_objc_msgSend_bool(IntPtr receiver, IntPtr selector, bool arg1);
    
        NSObject systemStatusBarObj;
        NSObject statusBarObj;
        NSObject statusBarItem;
        NSObject statusBarButton;
        NSObject statusBarImage;
    
        public Action ClickHandler { get; set; }
    
        public void Initialize()
        {
            statusBarObj = Runtime.GetNSObject(Class.GetHandle("NSStatusBar"));
            systemStatusBarObj = statusBarObj.PerformSelector(new Selector("systemStatusBar"));
            statusBarItem = Runtime.GetNSObject(IntPtr_objc_msgSend_nfloat(systemStatusBarObj.Handle, Selector.GetHandle("statusItemWithLength:"), -1));
            statusBarButton = Runtime.GetNSObject(IntPtr_objc_msgSend(statusBarItem.Handle, Selector.GetHandle("button")));
            statusBarImage = Runtime.GetNSObject(IntPtr_objc_msgSend(ObjCRuntime.Class.GetHandle("NSImage"), Selector.GetHandle("alloc")));
    
            var imgPath = System.IO.Path.Combine(NSBundle.MainBundle.BundlePath, "Contents", "Resources", "Platforms", "MacCatalyst", "trayicon.png");
            var imageFileStr = NSString.CreateNative(imgPath);
            var nsImagePtr = IntPtr_objc_msgSend_IntPtr(statusBarImage.Handle, Selector.GetHandle("initWithContentsOfFile:"), imageFileStr);
    
            void_objc_msgSend_IntPtr(statusBarButton.Handle, Selector.GetHandle("setImage:"), statusBarImage.Handle);
            void_objc_msgSend_bool(nsImagePtr, Selector.GetHandle("setTemplate:"), true);
    
            // Handle click
            void_objc_msgSend_IntPtr(statusBarButton.Handle, Selector.GetHandle("setTarget:"), this.Handle);
            void_objc_msgSend_IntPtr(statusBarButton.Handle, Selector.GetHandle("setAction:"), new Selector("handleButtonClick:").Handle);
        }
    
        [Export("handleButtonClick:")]
        void HandleClick(NSObject senderStatusBarButton)
        {
            var nsapp = Runtime.GetNSObject(Class.GetHandle("NSApplication"));
            var sharedApp = nsapp.PerformSelector(new Selector("sharedApplication"));
    
            void_objc_msgSend_bool(sharedApp.Handle, Selector.GetHandle("activateIgnoringOtherApps:"), true);
    
            ClickHandler?.Invoke();
        }
    }
    

    本来Apple的文档就不咋滴,看到这一坨彻底是震惊到了,居然还要熟悉苹果的那一套API才能搞得定,且不说这代码可读性和健壮性以及维护成本
    不过改改也能用,,,

    macOS发送通知无法弹出

    原生的接口似乎只找到DisplayAlert 和Toasts
    勉强凑活着用吧
    然而窗口非置顶的情况也就是程序没有获得焦点的情况下,通知窗口压根就不会弹出,也就是通知了也看不到,几乎半残
    一个对时间管理敏感的程序,到时间了还弹不出通知,那要来何用。。。

    于是在完成某一次commit之后,我在思考,趁着现在还没完成开发,切换成Avalonia是否还来得及

    答案是肯定的

    切换到Avalonia并不困难,在同目录先用新名字初始化一个空的Avalonia项目,把关键代码复制改改基本上一两个小时的就完成迁移

    踩坑Avalonia

    其实还好,
    由于Avalonia的UI都是自绘的,有时候看着美观性还差那么点意思,但是不影响
    真正的遇到的问题是有一个版本存在内存泄漏,折腾了好久,以为是自己的代码哪里没处理好,导致的泄漏
    终于在某一天看到官网的更新日志,修复了一个内存泄漏的问题,于是更新版本后神奇地修复了,大喜

    换成Avalonia后基本上Linux端也能用了,似乎没啥大毛病

    其他体验

    善用MVVM模式

    虽说前期用MAUI 折腾了一会,但是真正回顾下,切换到Avalonia后感觉上手真的非常快。
    无论MAUI还是Avalonia都是推荐MVVM开发模式,熟用绑定,基本上每个页面都比较清晰。
    尽管这里还是部份就在code behind把逻辑就写了。。。(反面教程)

    C# 开发效率

    这里用的是VS code, 在macOS下开发,偶尔搭配下Rider,目前为止还是比较丝滑。没有遇到大坑
    虽然不足windows + Vs 无敌,但是满足日常使用,即使换到Linux也能继续

    编译文件:
    在macOS打包成img 也不过是一百多M, 在Windows 和Linux只需几十M,而且可以打包成单个文件。
    也可以Aot编译,简直就是秒开,相比Electron 之类的,还是有不错的优势。

    运行效率

    MAUI的忘记对比资源占用了。
    最后的版本,在Windows 内存基本在六七十M,比较合理,
    在macOS和Linux下稍微多一点大概80-100M之间,也能接受

    结论

    MAUI 比较倾向移动端, 用来开发桌面软件,还是一言难尽
    推荐还是Avalonia,两者就上手难度而言,只要用过.NET的,稍微阅读下文档,其实就能把自己日常的需求开发起来,没有太大负担,值得拥有。
    至于是不是重复造轮子,见仁见智

    最后放上代码仓库:
    虽然没啥技术含量,有兴趣的可以看看 https://github.com/hoyho/iTimeSlot

  • 相关阅读:
    【单调栈】【概念讲解&&模板代码】
    c语言-浅谈指针(2)
    基于SSM开发实现校园疫情防控管理系统
    【腾讯云 TDSQL-C Serverless 产品体验】国产数据库遥遥领先
    一文带你认知定时消息发布RocketMQ
    tcpdump wireshark简单使用
    web前端期末大作业:JavaScript大作业——福五鼠动漫网页制作(6页)带轮播图效果 学生个人单页面网页作业 学生网页设计成品 静态HTML网页单页制作
    动态路由协议OSPF项目部署(二)
    计算机网络4小时速成:网络层,虚电路和数据包服务,ipv4,ABC类地址,地址解析协议ARP,子网掩码,ICMP忘记控制报文协议,路由选择协议,路由器
    嘉为蓝鲸CMP云管平台入选Gartner《中国云管理工具市场指南》
  • 原文地址:https://www.cnblogs.com/hoyho/p/18229682