Developing desktop apps from scratch: A feature checklist.

Author: alek-tron.com | alektron1@gmail.com

Starting a new software project is always exciting. Until the 80/20 rule kicks in and you realize that the exciting core features or algorithms only make up a fraction of what you need to deliver a polished, usable product. Of course this does not always have to be an issue. If you're building a tool just for yourself, it's easy to work around minor inconveniences or jank that is not getting in your way too much.

Once your tool starts being used by others however, especially non-developers, suddenly there is a long list of features missing that users just... expects to work. This is especially true when writing software from scratch, where no framework is doing the groundwork for you.

In this article I'll walk through a checklist of commonly seen and expected features that you should at least consider adding to your software if you intend to release it to a broader audience. The list is by no means exhaustive. It is based on my own experience developing a sizable in-house tool used by a variety of users over the last few years. The focus is on graphical, "Editor"-style applications with a typical "Open file -> Edit -> Save -> Close" workflow loop. Not every point will apply to every type of app, so use your own judgment.

Note: Most terminology and examples assume Windows as the development and target platform.

Open with...

On most operating systems a file type can be configured to always get opened with a specific application. This way, when double clicking a file in e.g. Windows File Explorer, that application will automatically be launched and open the file. This is a commonly used and expected feature but it is not automatically handled for you.

What happens under the hood is that the program is being launched with a filepath as its first command line parameter. It is effectively the same as launching your program via the command line like this: my_program.exe C:/MyFolder/Myfile.txt

It is then your programs responsibility to evaluate the command line arguments and trigger the code to open and view the specified file. Keep in mind however that there is no guarantee that the first argument is in fact a filepath. When using the command line, arbitrary arguments can be passed to your program. The first argument being a filepath is merely a convention.

File drag & drop

In addition to opening files from external sources, it can also be convenient to allow dragging files from e.g. the file explorer and drop them into the application. Implementing this can be done quite easily by handling the WM_DROPFILES message.

Single instance

I previously wrote about handling files being opened from external sources like the file explorer. By default this will launch a new instance of the program for every file. If your program supports editing/viewing multiple files at once, via tabs for example, this might not be the desired behavior. Instead every subsequent file should be opened in the instance that is already running.

Once again, Windows does not handle this for you automatically. When the program launches and detects that a file should be opened, it instead has to check for other running instances first. This could be achieved by using a named mutex. Only the first instance will be able to create the mutex, for all subsequent instances creating the mutex will fail. They now know that another instance is already running. Some IPC (Inter Process Communication) method must then be used to instruct the first instance to open the file instead. Possible IPC methods could be shared memory, named pipes or the WM_COPYDATA message. The new instance can then exit.

Preferably all this is being done before even showing a window to feel as seamless as possible.

Unsaved changes

Unless your program has a system in place that always automatically saves the current state of user data on exit and restores it at the next launch, it is important to prevent accidental loss of data when closing the application (or a tab). Usually this is being done by tracking files with unsaved changes and prompting the user when attempting to exit the application.

How exactly to implement this is up to you.

Recently opened files

A list of recently used files is often useful. Usually found in the menu bar under 'File -> Open recent' or on some sort of splash screen. If you are feeling fancy you can also make use of Windows' SHAddToRecentDocs to add files to the application's file list in the start menu or its 'Jump list' (when right clicking the app's icon in the taskbar). As is often the case with Windows however, this feature can be a bit tricky to get to work reliably. Among other things, a AppUserModelID must be set. It is also affected by various registry values.

DPI awareness

By default Windows assumes all applications to NOT be DPI aware. You can use SetProcessDpiAwareness or a manifest file to inform Windows that your application is DPI aware. If you do not do this, functions that should return DPI dependent information like GetDpiForWindow will always assume a DPI of 96. Windows will also attempt to automatically scale your application for displays with a DPI higher or lower than 96. However, this is only a post-processing effect, suffering the same undesired side-effects you get when trying to scale a regular image in e.g. Photoshop. Blurryness in the case of upscaling and loss of detail in the case of downscaling.

Of course, it is important that your application is in fact DPI aware, when specifying a DPI awareness level. You are now responsible for scaling your UI and fonts accordingly. Depending on the DPI awareness level you specify, you also want to handle the WM_DPICHANGED message to gracefully handle setups with e.g. multiple monitors with different DPIs.

Save/Restore monitor

When closing an application and opening it again, most users using a multi monitor setup expect the window to open on the same monitor that they closed it on. Arguably this may or may not be the behavior you want but IF you want it, once again, Windows does not handle this for you.

There is no one true method to do this. You just have to somehow save the window state on close, e.g. in a config file, and read and restore it on the next launch. How exactly you handle this is up to you.

Unicode support

Unless you are 100% certain that you will never need it, and I'm not sure you ever can be, your programs should support Unicode from the very beginning. Wether you use UTF-8, UTF-16 or UTF-32 is up to you, but decide on ONE encoding to be used in the whole codebase and do yourself a favor and don't let that encoding be ASCII. Even if your application does not support any other language than, say, English for its UI, you still want to be able to process Unicode strings. For example when handling filepaths or usernames.

For a refresher on Unicode and its encodings I recommend this video by Nic Barker or this article by Joel Spolsky.

Undo/Redo

If you are anything like me, you do not need to be reminded that any mature editor should support undo/redo. It's always in the back of my head when designing any system. Depending on the complexity of your data model, implementing an undo/redo system can range from being trivial to quite complex. Either way, it should not be postponed for too long as shoehorning it in at a later stage is often even harder.

I will not go into the details of how to design an undo/redo system here but I can recommend this article by Max Liani on the topic.

De-/Serialization

In and of itself serialization and deserialization probably does not need a reminder. The requirement to save/load user data will come up sooner rather than later. You can't really get around it and it's usually not very difficult to implement. It's just busywork. Still there are some aspects to it that should be kept in mind as they will come up eventually. I will talk about them in the next few chapters.

Versioning

You want changes to your serialized format to be backwards compatible. Files that got saved with an older version of your software should still be able to be opened by newer versions. This can be as easy as using a format like JSON and only ever adding fields. During deserialization you can then easily check if a new field exists and if it doesn't, just use a defaul value.

But there may also be cases where bigger, breaking changes to the format must be made. In that case it helps to add a version number to your files from the very beginning. There are several ways to achieve this.

A version number can be added to the serialization format you are already using e.g. just another JSON field. This however makes querying versions potentially expensive since the whole JSON string must be parsed first. More elegant solutions might store the version number independently of the actual data. For example in the file header (platform dependent). My favorite method packs the actual data into an archive file (e.g. ZIP) together with a separate metadata file containing the version number and possibly other metadata. This metadata file can then relatively easy be queried and read from the ZIP file without reading the whole file.

Unity uses a different approach where it creates a separate file for each asset file to store its metadata. It's a simple approach that allows quick iteration and querying of metadata but requires the users to be aware of them. Otherwise moving/renaming of files will cause issues if the metadata file is not moved/renamed as well. I will talk about a similar issue in a bit more detail in the next chapter.

File references

Depending on your application's use case, files may need to reference other files. You can do this by simply storing a filepath. Another commonly used method is to use some kind of GUID (Globally Unique Identifier). As with version numbers this requires you to have a system to store and (efficiently) query the GUID of another file.

Names/Paths vs. GUID is a whole other discussion. Both have their advantages and disadvantages. In my personal experience GUIDs sound nice on paper but cause more trouble than it's worth compared to regular filepaths. When using GUIDs, files could be moved or renamed and the system will still be able to find them and resolve references. While convenient at first, users are required to be aware of and understand this 'feature'. A cryptic error message like 'File reference with GUID XYZ could not be resolved' won't mean much to the average non-programmer user. And even if it does, the GUID must be easily accessible and editable or the issue can not be resolved. This possibly requires extra tooling.

A regular filepath on the other hand might break easily when files get moved or renamed but at least this is something the average user usually understands. An error message like "File C:/SomeFolder/SomeFile.txt could not be found" is much more straightforward, especially if the same user just recently renamed said file. In my experience the simplicity of filepaths outweighs the complexity and opaqueness of GUIDs.

Whatever method you may choose, keep in mind that those references can and will break. For those scenarios you want some kind of system in place to allow the user to interactively fix those broken references.

Multi-user file access

Collaborative editing that allows multiple users to simultaneously work on the same files/data has become quite popular. It really is a nice feature to have but even if your application does not support collaborative editing, you want to make sure that concurrent file access is somehow handled gracefully. The worst case scenario of two or more users simultaneously working on the same file on a shared drive and overwriting each others changes must be avoided.

On a low level this can be achieved by making use of the operating system's intended file sharing mechanisms when opening a file. On Windows, when opening a file via CreateFile a share mode must be specified to indicate how other processes can interact with the file while the current process owns the file handle. The simplest approach is to disallow all file operations and keep the file handle open for as long as the file is in use. In practice however you'd usually want to at least allow other processes to read the file. This way another user can still open and inspect the file, it just can not be overwritten. Make sure that the other user is properly informed of the fact that he will not be able to save the file, should he make any changes. It's a simple approach and has the additional advantage that the file is not only safe from changes made by your software but also any other software out of your control that could possibly try to access it.

On a higher level you could once again store some sort of metadata in your file (or a separate file) to indicate to other users that this file is currently being edited. This has the disadvantage of only being enforced by your application/ecosystem but gives you more control. In addition to the editing flag, one could also store the name of the user or other additional information that can then be displayed to other users accessing the file. In the case of a shared network drive, this flag would stay active even if the user loses connection. This can be seen as an advantage or disadvantage, depending on your use case.

Backups

Depending on the type of application you may want to create backups before overwriting a file during a save operation. Blender for example creates up to 2 rolling backup files every time you save over your existing .blend file. A rolling backup system is simple to implement but has its drawbacks. A user that saves very frequently may overwrite all the backup files before even noticing a mistake that they would want to rollback to. A more advanced approach could use one backup file to overwrite on every save, another one that only gets overwritten when it is older than hour and so on.

In addition to backups, an autosave system might also be of advantage.

Crash reports

Sometimes your users may run into a crash that you just can not reproduce locally. A crash like this is often a pain to figure out, especially if the user can not even reliably reproduce it themselves. In such cases crash dumps can be invaluable. Log files are fine but in my experience a much better solution are Minidumps.

A minidump file can be created without access to the build's PDB files, which makes them a great option in cases where you do not want to share those with your users since they could offer insight into the internals of your possibly proprietary software. They can then later be combined with the PDB files and the executable in a debugger to recreate the state of the application during the crash. It effectively gives you limited access to all the tools you would usually use to debug an application locally. Traversing the call stack, inspecting variables/memory etc. Of course you can not continue execution. Access to variables and memory might also be limited depending on the options the minidump was created with. A full memory image will give you the most insight but will also produce much larger files.

Either way, minidumps are invaluable in finding those annoying user side crashes. In the best case scenario you have a system in place to (semi-)automatically allow the users to send you the dump files. You should also always archive every build that gets deployed together with its PDB files. The PDBs and binaries must belong to the exact same build or you will run into issues trying to analyse them in combination with the minidump later.

Updates

Not everybody is a fan of daily update notifications, me included. Nonetheless, if you intend to offer automatic updates (or update notifications) for your application once it is deployed to your users, you should include a system for that in your very first version. Once the cat is out of the bag you will otherwise not be able to reach your users.

At its simplest form, an HTTP request to a webserver returning the most recent release version could already be enough. Just to show the user that a new version is available. I also recommend embedding a version number directly into the .exe file. On Windows you can then retrieve the version via GetFileVersionInfoExW.