Prompt Playground 7月开发记录(2): Avalonia 应用开发

发布时间 2023-08-08 11:42:49作者: 宵伯特

Prompt Playground 7月开发记录(2): Avalonia 应用开发

仅以此文记录开发过程中遇到的问题和个人的解决方案,如若有理解偏差或者更好的解决方案,欢迎指正。

客户端的开发的确不同于Web应用的开发。习惯了Web应用开发的模式之后,刚开始上手Avalonia应用的开发,就遇到了很多问题,其中比较多的还是UI相关的问题,还有就是状态管理的问题。

界面之上

给主菜单添加Icon

Icon的添加直接使用了Icons.Avalonia中的Material Design Icons.

其中提供了在菜单项上添加icon 的直接方法。

<MenuItem Header="About" i:MenuItem.Icon="fa-solid fa-circle-info" />

但是实际使用下来,发现只能给子菜单添加icon,主菜单无法添加。
于是就另辟蹊径,直接自定义主菜单的 Header。

<MenuItem>
    <MenuItem.Header>
        <i:Icon Value="mdi-menu"/>
    </MenuItem.Header>
</MenuItem>

在菜单栏内展示状态和选项

菜单栏的定义往往直接<Menu DockPanel.Dock="Top">就可以了。

但是如果没有那么多的菜单项的话,就会显得很空,于是就想在菜单栏内添加一些状态信息和选项。

如何继续使用<Menu>标签的话,其他的组件也会有菜单的样式,于是就做一些简单的调整,整体使用 <WrapPanel> 包裹。

<DockPanel>
    <WrapPanel DockPanel.Dock="Top">
        <Menu>
            <MenuItem>
                <MenuItem.Header>
                    <i:Icon Value="mdi-menu"/>
                </MenuItem.Header>
            </MenuItem>
        </Menu>
        <TextBlock Text="Status"/>
    </WrapPanel>
</DockPanel>

WrapPanel 和 StackPanel 类似,两者都是用来包裹其他组件的,WrapPanel默认是横向的,会换行,而StackPanel默认是纵向的。

实现简单的状态栏

状态栏和菜单栏类似,也是使用<DockPanel> + <StackPanel> ,只不过是放在了底部。

同时为StackPanel添加上一个默认的background,这样就可以看到状态栏了。
StackPanel中添加一个<WrapPanel>,里面就可以放置更多的状态组件了。

<StackPanel DockPanel.Dock="Bottom" Background="LightGray">
    <WrapPanel>
        <TextBlock Text="{{ Binding Status }}"/>
    </WrapPanel>
</StackPanel>

全局进度条

进度条本身只需要使用<ProgressBar>就可以了,但是为了突出一个全局的效果,就需要像菜单栏和状态栏一样,放在<DockPanel>中,例如固定在底部,另外再加上一个IsVisible的绑定,控制其显示和隐藏。

<DockPanel>
    <ProgressBar DockPanel.Dock="Bottom" Value="{Binding Progress}"
    IsVisible="{Binding Loading}"
     />
</DockPanel>

Tab栏上加按钮

Tab可以方便的做视图的切换,例如文件的切换等,但是如果Tab栏的内容不多的话,多少感觉有些空间的浪费,于是就想Tab栏右侧的位置是否将一些按钮放在那里。

在Avalonia的组件中没有找到直接类似的组件,大部分的布局控件并不会将组件堆叠在一起,所以最后使用了<Grid>来实现。

Grid 本身需要设置行列来确定组件的位置,但是如果不设置的话,组件的位置就可可能重叠。由此给操作按钮放在一个WrapPanel中,然后将WrapPanel放在Grid中,同时设置WrapPanelHorizontalAlignmentRight,这样就可以将WrapPanel放在右侧了。

<Grid > 
    <TabControl TabStripPlacement="Top" >
        <TabItem>
            <TabItem.Header>
                <i:Icon Value="mdi-file-edit-outline"/>
            </TabItem.Header>
            <ae:TextEditor
        Classes="editor"
        Document="{Binding Prompt}"
            />
        </TabItem>
        <TabItem>
            <TabItem.Header>
                <i:Icon Value="mdi-cogs"/>
            </TabItem.Header>
            <ae:TextEditor
        Classes="editor"
        Document="{Binding Config}"
        SyntaxHighlighting="Json"
            />
        </TabItem>
    </TabControl>
    <WrapPanel HorizontalAlignment="Right">
        <Button IsVisible="False" Click="OnGenerateButtonClick" HotKey="Ctrl+Enter"/><!-- 一个隐藏的按钮添加额外的快捷键 -->
        <Button Classes="MainBtn" Click="OnGenerateButtonClick" i:Attached.Icon="mdi-play" HotKey="Ctrl+G"  IsVisible="{Binding !IsGenerating}"/>
        <Button Classes="MainBtn" Click="OnCancelButtonClick" i:Attached.Icon="mdi-cancel" HotKey="Ctrl+C"  IsVisible="{Binding IsGenerating}"/>
        <Button Classes="MainBtn" Click="OnSaveButtonClick" i:Attached.Icon="mdi-content-save-outline"  HotKey="Ctrl+S"  IsEnabled="{Binding IsChanged}"/>
    </WrapPanel>
</Grid>

类似的,文本框上加状态信息等,也都是使用Grid定位即可实现。

界面之下

配置文件的加载和保存

为了便于长期使用,需要将配置文件保存到本地,同时在启动时加载配置文件。

需要确定两个问题:

  1. 配置文件保存在哪里?
  2. 配置何时加载?

至于配置文件的格式就不多讨论,常用的json格式也好,ini格式也好,只要便于读取和保存就行。

如果将配置文件保存在应用的目录下,可能会随着应用更新或者迁移导致配置文件丢失,因此最好将配置文件保存在用户目录下或者操作系统为应用提供的目录下。

例如使用以下方法就可以获取用户目录下配置文件的地址了。

const string _configFileName = "config.json";

var profile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);

var configPath = Path.Combine(profile, _configFileName);

配置作为全局的属性,需要在应用启动的时候加载,于是就可以将配置放在MainViewModel中,在初始化的时候加载。

public partial class MainViewModel : ViewModelBase
{
    public ConfigViewModel Config { get; set; } = ConfigViewModel.Load();
}

public partial class ConfigViewModel : ViewModelBasse {

    public static ConfigViewModel Load(){
        // 读取配置文件
    }
}

不同窗口之间的数据同步

不同窗口之间的数据传输重要还是使用ViewModel。
在Avalonia中,窗口的数据对象均为 DataContext,因此就可以通过设置DataContext来传递数据。

例如在主界面打开配置窗口的时候,将配置窗口的DataContext设置为MainViewModel中的Config,这样就可以在配置窗口中修改配置,同时主界面也能够获取到修改后的配置。

private void OnConfigClick(object sender, RoutedEventArgs e)
{
    var configWindow = new ConfigWindow()
    {
        DataContext = model.Config,
        ShowInTaskbar = false,
        WindowStartupLocation = WindowStartupLocation.CenterOwner
    };

    configWindow.ShowDialog(mainWindow);
}

界面之外

生成安装包

为了便于应用的分享和使用,就需要为应用生成安装包。
当然编译后打包的文件也可以直接分享,但是这样的话,用户就需要自己去下载覆盖更新,而且也不方便应用管理。同时如果支持自动更新的话,也不方便更新。

经过探索,找到了InnoSetup这个工具,可以将编译后的文件打包成安装包,同时还可以自定义安装界面,非常方便。

只需要为构建后的应用添加一个脚本文件,然后使用InnoSetup打包就可以了。InnoSetup 支持向导式的脚本生成,对于没有使用经验的人来说,也是相当方便了。

参与

Prompt Playground目前在Github上开源,使用.NET 7 + AvaloniaUI 进行开发,支持跨平台,欢迎大家参与。
如果有任何问题或者建议,也欢迎提issue或者PR。


知识共享许可协议本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。