Writing a Flutter Windows Clipboard plugin using C++
Writing a Flutter Windows Clipboard plugin using C++
Flutter Desktop has been an important tool for us as we work to build a native desktop version of Rows. However, Flutter does not always offer the same features that we want to implement in our app. Sometimes we have to get creative.
For example, copying clipboard data is an important aspect of Rows’ capabilities. How can we ensure that multiple data formats are supported in our native desktop environment?
Keep reading to see how we did it!
Rows supports two types of clipboard data, text and HTML.
Texts are straightforward. They can be inserted into columns or input bars, or even tab-separated values that are inserted into multiple cells at a time.
HTML, on the other hand, is trickier. Using the Rows application when we copy content from the table, we insert our own HTML format into the system’s HTML clipboard. This ensures that when we paste the content into another Rows application, we keep the information about cell styling, formulas and interactions.
As is, Flutter doesn’t really support any clipboard format other than text. To resolve this, we ended up writing a clipboard plugin that supports other formats. We mainly focused on HTML and text formats since these are our needs, but the code could be extended to enable support to other content types as well.
I will walk through how we did this implementation for Windows and, more importantly, try to explain the challenges involved since the Win32 API can be tricky sometimes. Most of the concepts and challenges I’m going to mention here apply to other Windows plugins as well.
I will be focusing on the Flutter Windows plugin implementation only, so if you are not familiar with how to create a plugin or work with Platform Channels, please check out these excellent Flutter resources first.
Why go native?
Each platform has a wide variety of supported clipboard content types and has its own way to get such content. Focusing on our needs at Rows, we added support for text and HTML content for macOS and Windows platforms only, since this covers our internal needs. We hope to write support for other platforms soon.
When it comes to macOS, it couldn’t be smoother. The APIs are simple enough to quickly get a plugin up and running. Also, we don’t have to do anything special for the HTML content - just insert the content into the correct clipboard and you are ready to go.
Windows is a different story. Win32 clipboard APIs are not the most friendly APIs to work with if you are not used to managing resources and locks. It is easy to lock the entire system’s clipboard up if errors are not handled properly. Encoding is also a big part of it since, to properly support Unicode, we must use the CF_UNICODETEXT format. This format is UTF-16, so we must convert it to UTF-8 to properly send it through a Flutter channel.
On top of that, the HTML format also requires a specific data format. One must add the headers and wrap the content properly on the defined tags.
Even though the dart win32 package is awesome, we chose C++ to write the Windows plugin. This was mostly because of my own past experience working with Windows APIs and C++. Additionally, people sometimes forget that although they are writing code in Dart with the win32 dart package, they still have to manage memory and resources. We think that it is easier to remember when you are writing code in C++. Not to mention, C++ is more fun to work with.
Windows Platform Channel
Before diving into specific clipboard API code, we need to understand how to send data back and forth through the channel.
As an example, we can use our method channel to read data from the clipboard plugin, for simplicity, we are going to use an int type to specify which clipboard format we want to get the data from, but you can make things cleaner by using enums.
The dart code looks like this:
With the dart code done, we can write the C++ counterpart.
When a channel is invoked, the HandleMethodCall from the plugin is called. With this method we can access the channel invocation data such as method name and parameters.
Flutter uses flutter::EncodableValue to transfer data through the channel, which is a std::variant type. A variant can only hold one of the Flutter-supported variants at a time. A simple way to retrieve a value is to use std::get_if, which returns the variant data for a given type, or null if no such data of the given type is in the variant.
Once we’ve got it from the clipboard, we call result→Success to send the data back to the channel. You can check more examples with more data types on Flutter’s own windows implementation.
The win32 clipboard API may be scary at first, but it is not that difficult to use. What makes it more difficult to understand are the documentation examples, which are way more complex than they should be.
To start using the clipboard, we have to ask the system for access to it. This is done by calling the OpenClipboard function. Here, it is important to notice that if an application opens the clipboard, it has to close it by calling CloseClipboard. Otherwise, the clipboard will remain open for your application and the system clipboard will stop working.
Using Dart win32, make sure to always use a try...finally block to close the clipboard after you opened it.
In C++ there is a common pattern to acquire resources called RAII pattern, we can use this not only to acquire the clipboard lock but also when we do global memory allocation.
The idea is simple: acquire a resource or allocate memory in a class constructor, and then release it in its destructor.
Every time we want to open the clipboard, instead of calling the Win32 API directly, we create an instance of our RaiiClipboard class, and when it is destroyed, the clipboard is closed.
We can use the same RAII strategy to handle global memory allocation, to set data into the clipboard, we must allocate the memory and make sure to release it at the end of the operation, this also will be used to read data.
Reading data from the Clipboard
Data can be read from the clipboard by calling the GetClipboardData function. It only receives one parameter which is the clipboard format. Windows has a wide range of standard formats and applications can also register their format.
The simplest operation we can do is reading content from the text clipboard. We will of course take advantage of our RAII implementation to make the code look cleaner.
With this code, we are already reading text from the clipboard and sending it back to the channel, but there is a problem with the text encoding.
Dealing with text encoding
Using the previous example, if we copy a text containing any special characters, such as Ã, you will notice that the content gets messed up when we read from the Dart String. That is because the CF_TEXT format uses the local machine ASCII table as encoding, and the channel expects UTF-8 encoded strings, so we need to convert the encoding of the text before sending it back to the channel.
To work around that, when reading and writing the text clipboard, we can use the CF_UNICODE, which is the intended format to deal with Unicode strings. Unfortunately, that alone won’t fix our problem, since this will return UTF-16 encoded wide strings. If we look at Flutter’s code, it does this conversion by using std::wstring_convert, but this is a deprecated method from the standard library. If you use strict build rules (as we do), it won’t allow any build warnings. so we must use Win32’s own conversion. It is not as beautiful but it gets the job done. I created a class called Encoding with methods to convert from and to UTF-8:
That should fix our issues with encoding. Now we can update our GetClipboardData to do the encoding conversion when it is reading from the CF_UNICODE format.
Now that our GetClipboardData has a format parameter, I also added a format check to make sure the format is available using the IsClipboardFormatAvailable function.
Writing data into the Clipboard
To write content into the clipboard, we must do the same encoding conversion as we did to read, and we can write the text using the CF_UNICODE format as well. We are going to use the Win32 SetClipboardData function, in addition to the clipboard format, this function also requires us to pass a handle to a previously allocated buffer to which we copy our data.
Here is the full example:
Even though Windows has a standard CF_HTML format, it doesn't come registered by default as a clipboard format for your application. Therefore, as soon as the plugin starts, we must register the CF_HTML as a custom format using the RegisterClipboardFormatA function. It feels counterintuitive, but that is how the API works. We can register it in the plugin constructor. Note that the name of the format has to be HTML Format.
Windows HTML clipboard format has its own document format, which has a header with information that most apps use to decode the content. We can't just set an HTML content string to the clipboard (well, you actually can, but most applications won't read the data properly due to the lack of the content header).
The header is pretty much a set of pointers to certain elements in the document.
It is easier for us to do this in Dart, so the plugin just receives an HTML string. In our Windows implementation, before sending it down to the channel, we add the required header and body. When reading from the clipboard, we strip this extra information and send only the HTML itself to the application.
Here is a full example of how this Dart class would look like,
During our previous steps, we took care to check the results of every Win32 API call and ensured that we were throwing an exception so we could properly handle it and send the result back to the application.
We can handle those by just adding a try…catch block to our HandleMethodCall method and return a more specific error on what went wrong back to the channel. If some of the Windows API calls had failed, we can get an error code by calling GetLastError and then use the FormatMessageA function to get a human-readable message of the error:
With that, when something fails, we are going to have a way more accurate description of the error through the channel error result.
Even though Flutter for Desktop is awesome, it doesn't come without some shortcomings. Some features that work well for mobile platforms may not be enough for desktop.
The clipboard itself is a perfect example of this challenge. Since we only deal with text clipboards on mobile, the current Flutter clipboard implementation works wonders. However, on Desktop settings where applications can exchange a wide variety of data formats, the current Flutter clipboard lacks essential features.