2020-12-27

3: Windows Clipboard with C#: in Multi-Formats

<The previous article in this series | The table of contents of this series |

.NET Framework is used, so applicable to Visual Basic.NET too. Many formats are supported.

Topics


About: C#

The table of contents of this article


Starting Context


  • The reader has a basic knowledge on C#.

Target Context


  • The reader will know how to get and set a composite of format-specific data from and to the Windows clipboard, in C#, or in Viusual Basic.NET.

Orientation


Many, but not all the, formats are supported by this technique.

All the formats are supported by a technique in C++ for Linux or Microsoft Windows, as will be introduced in a future article of another series (for Windows).


Main Body


1: Aim: to Implement Multiple Copy Buffers for a Word Processor


Hypothesizer 7
As I use Vim as a text editor, one of the benefits is that it has multiple copy buffers: I can copy a piece of text to the buffer 'a', another piece of text to the buffer 'b', etc.. That is huge help.

As I use LibreOffice Writer as a word processor, I want that functionality in it.

In fact, I tried to implement the functionality in Python, and it works partially, but not ideally.

The 2 dissatisfactions about the technique are 1) the possible datum formats have to be known a priori and 2) the supported datum formats are limited.

.NET Framework has a clipboard handling functionality, and I hoped that the functionality had the capability of handling the Windows clipboard in any possible way, because .NET Framework is Microsoft specific.

Well, supposing that the .NET Framework functionality is satisfactory, how can I call the C# code from the Python macro (as any macro cannot be written in C#)? . . . I will create an HTTP server in C#, which the Python macro will access.


2: Some Characteristics of the .NET Framework Clipboard Functionality


Hypothesizer 7
The .NET Framework clipboard functionality is characteristic in some points, as is seen in the API document.

While according to the Win32 API, each datum format is identified by a number, such numbers cannot be seen in .NET Framework; in .NET Framework, each datum format is identified by the name.

There is no need to explicitly open or close the clipboard.

What to be done for getting the clipboard data are basically straightforward: call 'System.Windows.Forms.Clipboard.GetDataObject ()' to get the multi-formats composite; call 'System.Windows.IDataObject.GetFormats (Boolean)' to get the format names from the multi-formats composite; call 'System.Windows.IDataObject.GetData (String, Boolean)' to get the datum of each format.

An issue is that the datum of each of many formats is gotten as a 'System.IO.MemoryStream' instance. . . . As stashing the instance itself seems not to work well, I read the bytes array out and save it.

Another issue is that the datum of each of some formats (like 'EnhancedMetafile' and 'MetaFilePict') cannot be gotten ('System.Windows.IDataObject.GetData (String, Boolean)' returns null). . . . Why? Well, in Microsoft Windows, it is not that the datum of any format can be just gotten in a uniform way as a bytes array. The datum of a format have to be gotten in a certain way, but .NET Framework has not made the effort of supporting all the possible formats. What can I do about it? Well, nothing, as far as I know (unless I use the Win32 API).

What to be done for setting the stashed data to the clipboard are straightforward: create an instance of 'System.Windows.DataObject' and call the 'System.Windows.IDataObject.SetData (String, Object, Boolean)' method of it per format to prepare a multi-formats composite; call 'System.Windows.Forms.Clipboard.SetDataObject (Object, Boolean)' to set the multi-formats composite to the clipboard.

The bytes arrays seem to be able to put in as they are.


3: My Code


Hypothesizer 7
As my aim is to store a history of clipboard data, I have created some classes as the storage, like these.

theBiasPlanet/coreUtilities/clipboardHandling/ClipboardFormatSpecificDatum.cs

@C# Source Code
namespace theBiasPlanet {
	namespace coreUtilities {
		namespace clipboardHandling {
			using System;
			
			public class ClipboardFormatSpecificDatum {
				private String i_formatName;
				private	Object i_datum;
				
				public ClipboardFormatSpecificDatum () {
				}
				
				public ClipboardFormatSpecificDatum (String a_formatName, Object a_datum) {
					i_formatName = a_formatName;
					i_datum = a_datum;
				}
				
				~ClipboardFormatSpecificDatum () {
				}
				
				public String getFormatName () {
					return i_formatName;
				}
				
				public Object getDatum () {
					return i_datum;
				}
			}
		}
	}
}

theBiasPlanet/coreUtilities/clipboardHandling/ClipboardFormatSpecificDataComposite.cs

@C# Source Code
namespace theBiasPlanet {
	namespace coreUtilities {
		namespace clipboardHandling {
			using System;
			using System.Collections.Generic;
			using theBiasPlanet.coreUtilities.collections;
			
			public class ClipboardFormatSpecificDataComposite {
				private NavigableLinkedHashMap <String, ClipboardFormatSpecificDatum> i_formatNameToDatumMap = new NavigableLinkedHashMap <String, ClipboardFormatSpecificDatum> ();
				
				public ClipboardFormatSpecificDataComposite () {
				}
				
				~ClipboardFormatSpecificDataComposite () {
				}
				
				public Boolean addFormatSpecificDatum (ClipboardFormatSpecificDatum a_formatSpecificDatum) {
					try {
						i_formatNameToDatumMap [a_formatSpecificDatum.getFormatName ()] = a_formatSpecificDatum;
					}
					catch (KeyNotFoundException) {
						i_formatNameToDatumMap.Add (a_formatSpecificDatum.getFormatName (), a_formatSpecificDatum);
					}
					return true;
				}
				
				public List <String> getFormatNames () {
					List <String> l_formatNames = new List <String> ();
					
					foreach (KeyValuePair <String, ClipboardFormatSpecificDatum> l_formatNameToDatumMapEntry in i_formatNameToDatumMap) {
						l_formatNames.Add (l_formatNameToDatumMapEntry.Key);
					}
					return l_formatNames;
				}
				
				public ClipboardFormatSpecificDatum getFormatSpecificDatum (String a_formatName) {
					return i_formatNameToDatumMap [a_formatName];
				}
			}
		}
	}
}

theBiasPlanet/coreUtilities/clipboardHandling/ClipboardFormatSpecificDataCompositesHistory.cs

@C# Source Code
namespace theBiasPlanet {
	namespace coreUtilities {
		namespace clipboardHandling {
			using System;
			using System.Collections.Generic;
			using theBiasPlanet.coreUtilities.collections;
			
			public class ClipboardFormatSpecificDataCompositesHistory {
				private NavigableLinkedHashMap <String, ClipboardFormatSpecificDataComposite> i_dataCompositeKeyToDataCompositeMap = new NavigableLinkedHashMap <String, ClipboardFormatSpecificDataComposite> ();
				
				public ClipboardFormatSpecificDataCompositesHistory () {
				}
				
				~ClipboardFormatSpecificDataCompositesHistory () {
				}
				
				public Boolean addDataComposite (String a_dataCompositeKey, ClipboardFormatSpecificDataComposite a_dataComposite) {
					try {
						i_dataCompositeKeyToDataCompositeMap [a_dataCompositeKey] = a_dataComposite;
					}
					catch (KeyNotFoundException) {
						i_dataCompositeKeyToDataCompositeMap.Add (a_dataCompositeKey, a_dataComposite);
					}
					return true;
				}
				
				public Boolean removeDataComposite (String a_dataCompositeKey) {
					i_dataCompositeKeyToDataCompositeMap.Remove (a_dataCompositeKey);
					return true;
				}
				
				public ClipboardFormatSpecificDataComposite getDataComposite (String a_dataCompositeKey) {
					return i_dataCompositeKeyToDataCompositeMap [a_dataCompositeKey];
				}
			}
		}
	}
}

The clipboard handling class is this.

theBiasPlanet/coreUtilities/clipboardHandling/MicrosoftWindowsClipboard.cs

@C# Source Code
namespace theBiasPlanet {
	namespace coreUtilities {
		namespace clipboardHandling {
			using System;
			using System.IO;
			using System.Windows;
			using System.Windows.Forms;
			using theBiasPlanet.coreUtilities.constantsGroups;
			
			public class MicrosoftWindowsClipboard {
				public static Boolean clearClipboard () {
					Clipboard.Clear ();
					return true;
				}
				
				public static ClipboardFormatSpecificDataComposite  getFormatSpecificDataComposite () {
					IDataObject l_dataObject = Clipboard.GetDataObject ();
					String [] l_datumFormatNames = l_dataObject.GetFormats (false);
					ClipboardFormatSpecificDataComposite l_formatSpecificDataComposite = new ClipboardFormatSpecificDataComposite ();
					foreach (String l_datumFormatName in l_datumFormatNames) {
						Object l_copiedFormatSpecificDatum = l_dataObject.GetData (l_datumFormatName, false);
						if (typeof (MemoryStream).IsInstanceOfType (l_copiedFormatSpecificDatum)) {
							MemoryStream l_copiedFormatSpecificDatumMemoryStream = (MemoryStream) l_copiedFormatSpecificDatum;
							Int64 l_copiedFormatSpecificDatumSizeMemoryStream = l_copiedFormatSpecificDatumMemoryStream.Length;
							byte [] l_copiedFormatSpecificDatumForMemoryStream = new byte [l_copiedFormatSpecificDatumSizeMemoryStream];
							for (int l_byteIndex = GeneralConstantsConstantsGroup.c_iterationStartNumber; l_byteIndex < l_copiedFormatSpecificDatumSizeMemoryStream; l_byteIndex ++) {
								l_copiedFormatSpecificDatumForMemoryStream [l_byteIndex] = Convert.ToByte (l_copiedFormatSpecificDatumMemoryStream.ReadByte ());
							}
							l_copiedFormatSpecificDatum = l_copiedFormatSpecificDatumForMemoryStream;
						}
						l_formatSpecificDataComposite.addFormatSpecificDatum (new ClipboardFormatSpecificDatum (l_datumFormatName, l_copiedFormatSpecificDatum));
					}
					return l_formatSpecificDataComposite;
				}
				
				public static Boolean setFormatSpecificDataComposite (ClipboardFormatSpecificDataComposite a_formatSpecificDataComposite) {
					IDataObject l_dataObject = new DataObject ();
					foreach (String l_datumFormatName  in a_formatSpecificDataComposite.getFormatNames ()) {
						ClipboardFormatSpecificDatum l_formatSpecificDatum = a_formatSpecificDataComposite.getFormatSpecificDatum (l_datumFormatName);
						Object l_copiedFormatSpecificDatum = l_formatSpecificDatum.getDatum ();
						l_dataObject.SetData (l_datumFormatName, true, l_copiedFormatSpecificDatum);
					}
					Clipboard.SetDataObject (l_dataObject, true);
					return true;
				}
			}
		}
	}
}


4: The Limitations


Hypothesizer 7
In addition to the limitation that the datum of each of some formats like 'EnhancedMetafile' cannot be gotten, in fact, not all formats can be even listed in this technique.

For example, the 'DataObject', 'Ole Private Data', 'Locale', and 'OEMText' formats are not listed by the 'System.Windows.IDataObject.GetFormats (Boolean)' method.


5: Testing


Hypothesizer 7
My test code is this.

@ Source Code
namespace theBiasPlanet {
	namespace coreUtilitiesTests {
		namespace clipboardHandlingTest1 {
			using System;
			using System.Collections.Generic;
			using theBiasPlanet.coreUtilities.clipboardHandling;
			
			public class Test1Test {
				public static void main (String [] a_argumentsArray) {
					test ();
				}
				
				private static void test () {
					ClipboardFormatSpecificDataCompositesHistory l_clipbardFormatSpecificDataCompositesHistory = new ClipboardFormatSpecificDataCompositesHistory ();
					while (true) {
						Console.WriteLine ("### Input 'S' (Set), 'G' (Get), 'D (Display)', or 'Q' (Quit):");
						Console.Out.Flush ();
						String l_userInput;
						l_userInput = Console.ReadLine ();
						String l_clipboardFormatSpecificDataCompositeKey;
						if (l_userInput == "S" || l_userInput == "G" || l_userInput == "D") {
							Console.WriteLine ("### Input the key:");
							Console.Out.Flush ();
							l_clipboardFormatSpecificDataCompositeKey = Console.ReadLine ();
							if (l_userInput == "S") {
								MicrosoftWindowsClipboard.clearClipboard ();
								MicrosoftWindowsClipboard.setFormatSpecificDataComposite (l_clipbardFormatSpecificDataCompositesHistory.getDataComposite (l_clipboardFormatSpecificDataCompositeKey));
							}
							else if (l_userInput == "G") {
								l_clipbardFormatSpecificDataCompositesHistory.addDataComposite (l_clipboardFormatSpecificDataCompositeKey, MicrosoftWindowsClipboard.getFormatSpecificDataComposite ());
							}
							else if (l_userInput == "D") {
								ClipboardFormatSpecificDataComposite l_clipboardFormatSpecificDataComposite = l_clipbardFormatSpecificDataCompositesHistory.getDataComposite (l_clipboardFormatSpecificDataCompositeKey);
								List <String> l_clipboardDatumFormatNames = l_clipboardFormatSpecificDataComposite.getFormatNames ();
								foreach (String l_clipboardDatumFormatName in l_clipboardDatumFormatNames) {
									ClipboardFormatSpecificDatum l_clipboardFormatSpecificDatum = l_clipboardFormatSpecificDataComposite.getFormatSpecificDatum (l_clipboardDatumFormatName);
									Console.WriteLine (String.Format ("### clipboard datum format name: {0:s}", l_clipboardDatumFormatName));
									Console.Out.Flush ();
								}
							}
						}
						else {
							break;
						}
					}
				}
			}
		}
	}
}

Note that the 'Main' (not that "main (String [] a_argumentsArray)") method (not shown above) has to be annotated with '[STAThread]'.

After I have copied a text piece on a 'cmd' terminal, I get an output like this for the 'G' -> 'A' -> 'D' -> 'A' inputs.

@Output
### clipboard datum format name: UnicodeText
### clipboard datum format name: Locale
### clipboard datum format name: Text
### clipboard datum format name: OEMText

After I have copied a text piece on a LibreOffice Writer instance, I get an output like this for the 'G' -> 'A' -> 'D' -> 'A' inputs.

@Output
### clipboard datum format name: Star Embed Source (XML)
### clipboard datum format name: Rich Text Format
### clipboard datum format name: Richtext Format
### clipboard datum format name: HTML (HyperText Markup Language)
### clipboard datum format name: HTML Format
### clipboard datum format name: UnicodeText
### clipboard datum format name: Text
### clipboard datum format name: Link
### clipboard datum format name: Star Object Descriptor (XML)


References


<The previous article in this series | The table of contents of this series |