rubberduck-vba / Rubberduck

Every programmer needs a rubberduck. COM add-in for the VBA & VB6 IDE (VBE).
https://rubberduckvba.com
GNU General Public License v3.0
1.9k stars 298 forks source link

VB6 editor is missing menu and commandbar icons #3952

Open mansellan opened 6 years ago

mansellan commented 6 years ago

image

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.

mansellan commented 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.

bclothier commented 6 years ago

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.

WaynePhillipsEA commented 6 years ago

@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)

WaynePhillipsEA commented 6 years ago

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.

mansellan commented 6 years ago

This is going to need some kind of clipboard inspection. So far I have done the following:

  1. Outside of RD, created a custom CommandBarButton and pasted an image into it*
  2. Back in RD, programatically located this button and called Clipboard.GetData("Toolbar Button Face") and ("Toolbar Button Mask"), sent resulting memory stream into byte[] arrays.
  3. Clipboard.Clear
  4. Clipboard.SetData x2 (using same formats)
  5. Button.PasteFace

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.

mansellan commented 6 years ago

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.

bclothier commented 6 years ago

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?

Ref: http://www.lebans.com/image_faq.htm

mansellan commented 6 years ago

Looks like there's a bizarre bug in the Office8 commandbars. MVCE:

  1. Create a 16x16 bitmap in MS paint. Save as any color depth, then select all and copy to clipboard.
  2. Load VB6 without RD.
  3. Right-click toolbar and choose Customise.
  4. Navigate to a menu or toolbar button, right click and choose Paste Button Image.
  5. Click OK to leave 'customise toolbars' mode.
  6. Icon will only be visible when the menu item or toolbar button is enabled, or when the IDE is put back into 'customise toolbars' mode.

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).

mansellan commented 6 years ago

Getting there...

Expected format is a BITMAPINFOHEADER containing 16 RGBQUADs.

Still need to write a colour reducer and compute a mask.

mansellan commented 6 years ago

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.

mansellan commented 6 years ago

Looks OK with hand-editing.

image

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.

mansellan commented 6 years ago

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.

image

WaynePhillipsEA commented 6 years ago

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).

mansellan commented 6 years ago

@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.

bclothier commented 6 years ago

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.

mansellan commented 6 years ago

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.

mansellan commented 6 years ago

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"...