Cross-Platform Desktop GUI Application using .Net Core
Cross-platform development is great, so is .Net Core. But, as of the time of this writing, .Net Core doesn’t provide a cross-platform windowed GUI. Let’s try to build a desktop app with a cross-platform GUI.
So, what options do we have?
- GtkSharp: C# wrapper for the Gtk3 library
- Uno Platform: Builds mobile, web, desktop and embedded apps
- Avalonia: WPF/UWP-inspired XAML-based UI framework
- Qml.Net: Qt/Qml integration with .NET
- .Net MAUI: Multi-platform App UI from Microsoft
Yes, .Net MAUI sounds quite nice. It will be the Microsoft-official way to build desktop and mobile GUIs. But it is in its early development phase for now, will be shipped later 2020 with .Net 5 preview (.Net is the new name of .Net Core), and finally will target general availability with .Net 6 in November of 2021. When this is stable-available, it seems it’ll be de facto way of making GUIs over .Net.
You can view details of these choices by clicking their links at above list to their official pages. I have given GtkSharp the first shot, since Gtk2 is an old friend of mine. And since it succeeded, I didn’t try the rest. But they seem quite promising and nice looking as well. 👀👌
Let’s start. I did all development and deployment on Windows. At Visual Studio 2019, when no solution is loaded, open command prompt by Tools -> Command Line -> Developer Command Prompt, enter below command.
dotnet new --install GtkSharp.Template.CSharp
At the time of this writing, VS 2019 doesn’t support listing templates which are installed via command prompt at the new project dialog. So, at the prompt, go to the directory you’d like to work at, enter below command.
dotnet new gtkapp
This will create a project with a Gtk# GUI window. You can find more options about the “new” CLI command by typing merely “dotnet new”.
We are done with command prompt, close it, and open the project. When you save this project, VS will save it as a solution. Build and run to see below window. Clicking the button will alter the text, indicating signals (events) and listeners (handlers) are working as they should.
Open MainWindow.glade, this XML-based text file is the GUI layout format used by Gtk#, as in many other GUI libraries like Xamarin.Forms. I never liked designing a GUI via editing an XML, so let’s install a graphical editor for glade files. It’s called, Glade. 🤦♂️
At Glade web page there are some Windows program binaries but they are all previous versions and no good at all. So go to its gitlab page and do what’s described for Windows installation. Install MSYS2, which creates an isolated, linux-like package-based environment at C:\msys64. At its command line, enter below command.
pacman -S mingw-w64-x86_64-glade
Some side notes; pacman is the package manager of Arch Linux, and the installed Windows program binary is compiled by MinGW, a toolkit for porting linux apps to Windows. These all happen in the isolated environment of MSYS2.
Latest version of Glade is installed at C:\msys64\mingw64\bin\glade.exe by default. You may create a desktop shortcut if you like.
After running Glade, MainWindow.glade of our project can be opened directly for editing. Here is where GUI design is made. There are tutorials, user manual and API reference in case of need. For testing, I doubled the default content, resulting below test GUI.
We also have to alter the code to put the additional GUI functionality. For completeness, below are MainWindow.cs and MainWindow.glade. Program.cs remained unaltered.
MainWindow.cs:
using System;
using Gtk;
using UI = Gtk.Builder.ObjectAttribute;namespace GtkTestApp
{
class MainWindow : Window
{
[UI] private Label _label1 = null;
[UI] private Label _label2 = null;
[UI] private Button _button1 = null;
[UI] private Button _button2 = null;private int _counter1, _counter2;public MainWindow() : this(new Builder("MainWindow.glade")) { }private MainWindow(Builder builder) : base(builder.GetObject("MainWindow").Handle)
{
builder.Autoconnect(this);DeleteEvent += Window_DeleteEvent;
_button1.Clicked += Button1_Clicked;
_button2.Clicked += Button2_Clicked;
}private void Window_DeleteEvent(object sender, DeleteEventArgs a)
{
Application.Quit();
}private void Button1_Clicked(object sender, EventArgs a)
{
_counter1++;
_label1.Text = "This is incrementing: " + _counter1;
}private void Button2_Clicked(object sender, EventArgs a)
{
_counter2--;
_label2.Text = "This is decrementing: " + _counter2;
}
}
}
MainWindow.glade:
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.36.0 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<object class="GtkWindow" id="MainWindow">
<property name="can_focus">False</property>
<property name="title" translatable="yes">Example Window</property>
<property name="default_width">480</property>
<property name="default_height">240</property>
<child>
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="column_homogeneous">True</property>
<child>
<object class="GtkLabel" id="_label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="vexpand">True</property>
<property name="label" translatable="yes">Hello World!</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<property name="xalign">0.5</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="_label2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="vexpand">True</property>
<property name="label" translatable="yes">Hello World!</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<property name="xalign">0.5</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="_button1">
<property name="label" translatable="yes">Click me!</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="_button2">
<property name="label" translatable="yes">Click me!</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
</object>
</child>
<child type="titlebar">
<placeholder/>
</child>
</object>
</interface>
Now we’ve completed the Windows application. Since we used a multi-platform framework and a multi-platform library, we can easily deploy the app to whichever platform we want. At VS, go to Build -> Publish, select the option to publish to folder. At the summary section, click any of the edit icon-buttons. Publish profile settings window will show.
I use framework-dependent mode because self-contained mode puts the whole .Net Core runtime, which is necessary to run the app at target platform, into the resulting app. It may sound nice but remember, if you install 2 self-contained apps, you will double-have the runtime, wasting disk space, and not nice. 😒 In addition, most runtime are already installed by default on most platforms. And if not, they are pretty easy to install.
I don’t go with portable targeting since it almost always requires additional adjustments at target side, like ‘opening the executable’ with the corresponding runtime. Under the hood, this targeting creates Intermediate Language, which requires Just-In-Time compiling at app start.
Note that this is cross-compile, meaning the compiler is compiling an app that is meant to run on another platform. This would mostly happen when compiling mobile apps at desktop development machines, but now we have it this easily. 😉
I published to win-x64, linux-x64 and osx-x64. One can publish to linux-arm to target i.e. Raspberry Pi. When resulting apps are moved to their corresponding platforms and are executed, below GUIs showed up and all is functional. 🎉
Windows Gtk Runtime
If app gives an error about Gtk or doesn’t start at all, it means the Gtk runtime library is not installed. If this happens, do one of the following:
If you have admin rights, go to here, download the latest gtk3-runtime installer and install it.
If you don’t have admin rights, go to here, download the latest gtk-3 zip file, extract it to a user local folder (e.g. %appdata%\gtk-3). Then modify PATH variable so that the library can be found. Go to Control Panel -> User Accounts -> Change my environment variables, double click to Path under User variables, add the user local folder.
macOS Gtk Runtime
If app gives an error about Gtk, it means the Gtk runtime library is not installed. If this happens, open a terminal at macOS and enter below command.
brew install gtk+3
That’s it! We have a cross-platform desktop GUI application using .Net Core. From this point, possibilities are quite a lot since we both have multi-platform windowed GUI and have .Net Core at our disposal 😎