The Rows blog

Share this post
Writing a Flutter Windows Clipboard plugin using C++
blog.rows.com

Writing a Flutter Windows Clipboard plugin using C++

Victor Botamedi
Jun 14
7
Share this post
Writing a Flutter Windows Clipboard plugin using C++
blog.rows.com

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:

  // Our platform channel   static const MethodChannel _channel = MethodChannel('clipboard_plugin');    Future<String?> getClipboardData(int dataType) =>       // Invokes the getClipboardData method sending an int        // as parameter and expects a String as a return type.       _channel.invokeMethod<String>(         'getClipboardData',         dataType, // 0 = text, 1 = html.       );
source code

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.

	void ClipboardPlugin::HandleMethodCall(         const flutter::MethodCall<flutter::EncodableValue> &method_call,         std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)     {         // Checks which method is being called.         if (method_call.method_name().compare("getClipboardData") == 0)         {             // Gets the call argument.             // Here we expect a int argument, if no argument was sent, we return             // an error result back to the channel.             const auto* data_type = std::get_if<int>(method_call.arguments());             if (!data_type)             {                 result->Error("Missing required type parameter", "Expected int");                 return;             }             // We will implement GetClipboardData later on.             auto data = GetClipboardData(data_type);             result->Success(flutter::EncodableValue(data));         }      }
source code

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.

Clipboard Basics

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.

class RaiiClipboard { public:     RaiiClipboard()     {         // Opens the clipboard in the class constructor.         if (!OpenClipboard(nullptr))         {             throw std::runtime_error("Can't open clipboard.");         }     }      ~RaiiClipboard()     {         // Close it in the destructor.         CloseClipboard();     }  private:     RaiiClipboard(const RaiiClipboard &);     RaiiClipboard &operator=(const RaiiClipboard &); };
source code

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.

template<class T> class RaiiGlobalLock { public:     explicit RaiiGlobalLock(HANDLE data_handle)         : m_data_handle(data_handle)     {         // Acquires the global lock in the constructor.         m_plock = static_cast<T*>(GlobalLock(m_data_handle));         if (!m_plock)         {             throw std::runtime_error("Can't acquire lock on clipboard data.");         }     }      ~RaiiGlobalLock()     {         // Releases in the destructor.         GlobalUnlock(m_data_handle);     }      // Getter to access the pointer data.     T* Get()     {         return m_plock;     }  private:     HANDLE m_data_handle;     T* m_plock;      RaiiGlobalLock(const RaiiGlobalLock &);     RaiiGlobalLock &operator=(const RaiiGlobalLock &); };
source code

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.

std::string ClipboardPlugin::GetClipboardData() {     // Opens the clipboard. Automatically closes it when the function     // scope finishes.     RaiiClipboard clipboard;     // Gets the data from the text clipboard     auto data = ::GetClipboardData(CF_TEXT);     if (nullptr == data)     {         throw std::runtime_error("Can't get clipboard data.");     }     // Aquire the global lock to the data.     RaiiGlobalLock<const char> lock(data);     // Returns it as a string.     return std::string(lock.Get()); }
source code

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:

#include <windows.h> #include <string>  namespace Encoding {     // Converts a wide string to UTF-8.     std::string WideToUtf8(const std::wstring& wstr)     {         int count = WideCharToMultiByte(             CP_UTF8,              0,              wstr.c_str(),              static_cast<int>(wstr.length()),              nullptr,              0,              nullptr,              nullptr);         std::string str(count, 0);         WideCharToMultiByte(             CP_UTF8,              0,              wstr.c_str(),              -1,              &str[0],              count,              nullptr,              nullptr);         return str;     }      // Converts an UTF-8 string to a wide string.     std::wstring Utf8ToWide(const std::string& str)     {         int count = MultiByteToWideChar(             CP_UTF8,              0,              str.c_str(),              static_cast<int>(str.length()),              nullptr,              0);         std::wstring wstr(count, 0);         MultiByteToWideChar(             CP_UTF8,              0,              str.c_str(),              static_cast<int>(str.length()),              &wstr[0],              count);         return wstr;     } }
source code

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.

std::string ClipboardPlugin::GetClipboardData(unsigned int format_id) {     // Opens the clipboard. Automatically closes it when the function     // scope finishes.     RaiiClipboard clipboard;      // Before reading from the clipboard, check if the specified     // format is available.     if (!IsClipboardFormatAvailable(format_id))     {         return "";     }      // Gets the data from the text clipboard     auto data = ::GetClipboardData(format_id);     if (nullptr == data)     {         throw std::runtime_error("Can't get clipboard data.");     }     // Always use unicode when we are working with the text format.     if (CF_UNICODETEXT == format_id)     {         RaiiGlobalLock<const wchar_t> lock(data);         return Encoding::WideToUtf8(std::wstring(lock.Get()));     }      // Aquire the global lock to the data.     RaiiGlobalLock<const char> lock(data);     // Returns it as a string.     return std::string(lock.Get()); }
source code

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:

void ClipboardPlugin::SetClipboardData(unsigned int formatId, std::string data) {     RaiiClipboard clipboard;     HGLOBAL handle = nullptr;      // Optionally we can clear the clipboard before inserting new data.     EmptyClipboard();     // When working with Unicode, we must use a wchar_t buffer.     if (CF_UNICODETEXT == formatId)     {         // Converts the UTF-8 string coming from the channel to          // a wide string.         const auto wide_str = Encoding::Utf8ToWide(data);          // Allocates a buffer to write the data.         handle = GlobalAlloc(GMEM_MOVEABLE, wide_str.size() * sizeof(wchar_t) + 2);         if (nullptr == handle)         {             throw std::runtime_error("Failed to GlobalAlloc");         }         // Locks the handle to our buffer.         RaiiGlobalLock<wchar_t> globalLock(handle);         // Copies the data to our new buffer. Our RAII class will make sure         // to release the lock.         wcscpy_s(globalLock.Get(), wide_str.size() + 1, wide_str.c_str());     }     else      {         // Same steps to other clipboard formats, but here we won't         // need to convert the data encoding.         auto buffer_size = data.size() + 1;         handle = GlobalAlloc(GMEM_MOVEABLE, buffer_size);         if (nullptr == handle)         {             throw std::runtime_error("Failed to GlobalAlloc");         }         RaiiGlobalLock<char> globalLock(handle);         strcpy_s(globalLock.Get(), buffer_size, data.c_str());     }          // Finally sets the content into the specified clipboard format.     if (!::SetClipboardData(formatId, handle))     {         throw std::runtime_error("Failed to SetClipboardData");     } }
source code

HTML Format

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.

// A variable to keep track of the HTML format ID. unsigned int m_html_format;  ClipboardPlugin::ClipboardPlugin() {     m_html_format = RegisterClipboardFormatA("HTML Format"); }
source code

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,

class WindowsClipboard {   static const MethodChannel _channel = MethodChannel('clipboard_plugin');      static const _startFragmentTag = '<!--StartFragment-->';   static const _endFragmentTag = '<!--EndFragment-->';    Future<String> getClipboardHtmlData() async {     final text = await _channel.invokeMethod<String>(         'getClipboardData',         1, // 0 = text, 1 = html.       );     if (text == null) {       return '';     }     // If we got the data, then remove the extra Windows HTML tokens.     return _removeHtmlFragmentHeaders(text);   }    Future<void> setClipboardHtmlData(String data) async {     // Adds the HTML tokens to our HTML string.     var content = _createHtmlContent(content);     await _channel.invokeMethod<void>(       'setClipboardHtmlData',       content,     );   }    /// Wraps the content in the standard HTML clipboard format.   String _createHtmlContent(String fragment) {     const htmlTag = '<html>';      const header = 'Version:0.9\r\n'         'StartHTML:00000000\r\n'         'EndHTML:00000000\r\n'         'StartFragment:00000000\r\n'         'EndFragment:00000000\r\n'         '$htmlTag\r\n'         '<body>\r\n'         '$_startFragmentTag';      const footer = '$_endFragmentTag\r\n'         '</body>\r\n'         '</html>\r';      var content = header + fragment + footer;     final htmlStartIndex = content.indexOf(htmlTag);     content = content.replaceFirst(       'StartHTML:00000000',       'StartHTML:${htmlStartIndex.toString().padLeft(8, '0')}',     );      content = content.replaceFirst(       'EndHTML:00000000',       'EndHTML:${(content.length - 1).toString().padLeft(8, '0')}',     );      final fragmentStartIndex =         content.indexOf(_startFragmentTag) + _startFragmentTag.length;     content = content.replaceFirst(       'StartFragment:00000000',       'StartFragment:${fragmentStartIndex.toString().padLeft(8, '0')}',     );      final fragmentEndIndex = content.indexOf(_endFragmentTag);     return content.replaceFirst(       'EndFragment:00000000',       'EndFragment:${fragmentEndIndex.toString().padLeft(8, '0')}',     );   }    String _removeHtmlFragmentHeaders(String data) {     final htmlStartIndex = data.indexOf('<html>');     if (htmlStartIndex == -1) {       return data;     }     return data         .substring(htmlStartIndex)         .replaceAll(RegExp('$_startFragmentTag|$_endFragmentTag'), '');   } }
source code

Error Handling

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:

void ClipboardPlugin::HandleMethodCall(     const flutter::MethodCall<flutter::EncodableValue> &method_call,     std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {     try     {         // ...     }     catch (const std::exception &e)     {         auto error_message = GetSystemLastErrorMessage();         std::cerr << e.what() << std::endl                   << error_message << std::endl;         result->Error(e.what(), error_message);     } }  std::string ClipboardPlugin::GetSystemLastErrorMessage() {     std::ostringstream stream;     auto error = GetLastError();     LPSTR errorText = nullptr;     FormatMessageA(         FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS,         nullptr,         error,         MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),         (LPSTR)&errorText,         0,         nullptr);     if (nullptr != errorText)     {         stream << "System error code: " << error << std::endl                << "Error description: " << errorText;         LocalFree(errorText);     }     return stream.str(); }
source code

With that, when something fails, we are going to have a way more accurate description of the error through the channel error result.

Final thoughts

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.

You can explore our working plugin here. We call it Ditto. The Windows implementation is under ditto_windows, and you can also try the example app and explore the macOS code.

Share this post
Writing a Flutter Windows Clipboard plugin using C++
blog.rows.com
Comments

Create your profile

0 subscriptions will be displayed on your profile (edit)

Skip for now

Only paid subscribers can comment on this post

Already a paid subscriber? Sign in

Check your email

For your security, we need to re-authenticate you.

Click the link we sent to , or click here to sign in.

TopNewCommunity

No posts

Ready for more?

© 2022 Rows
Privacy ∙ Terms ∙ Collection notice
Publish on Substack Get the app
Substack is the home for great writing