Open mansellan opened 6 years ago
So it turns out I was wrong about using DLL calls. As an office commandbar, the only way is option 2.
It's possible that the unreliability is due to not registering the correct clipboard formats ("Toolbar Button Face" and "Toolbar Button Mask" in en locales). I also have an idea on how to make it locale-agnostic.
Just one more thought - the PasteFace
command strongly impiles that it's UI-related task. It may be necessary to ensure it is only executed on the UI thread, which you can do via the IUiDispatcher.InvokeAsync
.
@bclothier not necessary here. In general, you need not worry about COM calls happening in the wrong thread. They will be marshalled to the correct thread for you automagically. (This is somewhat different to the TypeLib API, due to the ~hack~ magic we use to grab the initial COM object)
Having to use the clipboard for this is a horrible, cumbersome design. I suspect the issues on my machine were something to do with the Office clipboard manager (in Office 2003) interfering with the underlying clipboard API calls. If having the icons is important enough, we could potentially hook the clipboard API calls that VB6 makes at this point, and redirect them to our own .NET implementations, so in effect not using the clipboard at all (remembering to unhook immediately after the PasteFace call of course). That might not be as easy as it sounds though.
This is going to need some kind of clipboard inspection. So far I have done the following:
Step 5 fails with a cryptic COM exception. But - using Button.CopyFace and Button.PasteFace works fine.
So my conclusion is that something is off with the contents of the clipboard after SetData as compared to CopyFace.
Btw, the need for a custom image in step 1 is due to CopyFace just sending the FaceId to the clipboard if it can.
Oh, and I agree - dumping the user clipboard is poor UX. It seems to have been the accepted way back in early COM add-in days, but doesn't mean it is now.
FWIW, in older versions of Access whenever you paste in an image, it got wrapped with EMF metadata and was saved with that extra wrapping. That essentially meant the picture was saved differently from its original format. It is very possible you are observing a similar thing going on here; what you are getting out contains all metadata and the PasteFace
won't have none of that; it only wants raw image file?
Looks like there's a bizarre bug in the Office8 commandbars. MVCE:
Note, this bug does not occur for icons created with the built-in editor. This is most revealing - the built-in editor creates 4bpp indexed images of a fixed 16-color palette (plus 1bpp for a mask, stored separately). It appears that Office 8 is incapable of generating 'greyed out' images for any other bitmap type, therefore this is the only workable format for RD VB6 (and presumably also Office 2000).
Getting there...
Expected format is a BITMAPINFOHEADER containing 16 RGBQUADs.
Still need to write a colour reducer and compute a mask.
Hmm... not got the colours or mask quite right, but done enough to see that conversion from 32bpp PNG to 4bpp BMP is gonna look absolutely dreadful, even at 16x16.
If we want icons in VB6, I think they're going to need separate low-colour resource files to be created and added.
Looks OK with hand-editing.
The format expected by PasteFace is a 4bpp indexed bmp with the right color palette, stripped of the first 14 bytes (which is the bitmap file header).
RD has around a dozen icons used on COM commandbars, I'll create low-color versions of them for this PR.
The Office 8 PasteFace method is very picky about formats. Not only must it be a 4bpp indexed BMP, it must have a fixed color palette with the indices in exactly the order it expects.
The upshot of this is that the only reliable way to create the images is using the built-in toolbar editor. BMPs created by any other means will not meet the above requirements. This includes simply using the .Net Bitmap class to parse the data prior to copying to the clipboard.
This means that we can only load the resources as a raw binary array, from a .dat file. To make this process maintainable, I'm writing a small WinForms tool that can capture images edited using the built-in toolbar, and save the .dat files out.
Just to throw it out there...
@mansellan in case you want to try hooking the clipboard API as I mentioned earlier in the thread, here's some untested code to get you started:
var Hook = EasyHook.LocalHook.Create(
EasyHook.LocalHook.GetProcAddress("user32.dll", "GetClipboardData"),
new Delegate_RDGetClipboardData(RDGetClipboardData),
this);
// Activate the hook on this thread only
Hook.ThreadACL.SetInclusiveACL(new int[] { 0 });
[System.Runtime.InteropServices.UnmanagedFunctionPointer(System.Runtime.InteropServices.CallingConvention.StdCall)]
delegate IntPtr Delegate_RDGetClipboardData(uint format);
static IntPtr RDGetClipboardData(uint format)
{
const uint CF_BITMAP = 2;
if (format == CF_BITMAP)
{
IntPtr dataOutputHandle = Marshal.AllocHGlobal(bitmapBytes.length);
Marshal.Copy(bitmapBytes, 0, dataOutputHandle, bitmapBytes.length);
return dataOutputHandle;
};
return IntPtr.Zero;
}
You'd also probably want to hook the OpenClipboard/CloseClipboard API calls too (no-op).
@WaynePhillipsEA that's hugely appreciated, and very timely. Something odd is going on atm - I'm putting the exact same bytes in the exact same place on the clipboard as is done by CopyFace, and it's not wroking correctly. Office is doing something else under the covers that I'm not seeing atm.
I'm pretty sure it's similar to what I mentioned earlier, what you write in, isn't what you read out. It is likely doing more than just eating some bytes.
Possibly - but what I don't understand if that's the case is why I can (using the built-in toolbar editor) copy from one instance of VB6 and paste into another. I guess its possible it's doing some extra inter-proc communication, but feel its more likely to just be eating what's on the clipboard.
That said, I've just dowloaded a clipboard monitor from here, and I can see now that it uses more than just Toolbar Button Face and Toolbar Button Mask - it also uses Device Independant Bitmap, System.Drawing.Bitmap, Bitmap and Format 17.
If it's just a case of serializing \ deserializing those as well, then I think my approach is still possible.
I've parked this to focus on more pressing VB6 issues for now.
But... an interesting thought occurred. VB6 will happily display high-color toolbar images, it's just incapable of generating the 'greyed out' versions when the item is disabled, so they go missing. In theory, it should be possible assign a low color image when disabling the button, and swap the high color one back in when (re-)enabling.
Overkill for sure, but would sure help VB6 look a little less "1998"...
This is due to neither of the options from the VBA CommandBarButton.ApplyIcon method being appropriate for VB6:
Accordingly, these approaches have been removed from the VB6 CommandBarButton.ApplyIcon method, which is currently no-op.
It's highly likely that icons can be set through native DLL calls, which should solve this issue.