How to Access WPF controls using ElementHost? - excel

I'm building an Add-in for Excel. I wanted to use WPF controls, so I followed this tutorial
https://learn.microsoft.com/en-us/visualstudio/vsto/using-wpf-controls-in-office-solutions?view=vs-2022
I added some tab controls to get my feet wet, I particularly wanted to programmatically go to a tab item if some condition was met upon clicking a button in the Ribbon, I was able to access the header text of the tab item, but I was not able to modify it or set it to isSelected.
This is the code that I'm using:
Imports Microsoft.Office.Tools
Imports Microsoft.Office.Tools.Ribbon
Public Class Ribbon1
Private myUserControl1 As MyUserControl
Private myCustomTaskPane As CustomTaskPane
Private Sub btnCheck_Click(sender As Object, e As RibbonControlEventArgs) Handles btnCheck.Click
' Show the taskpane
myUserControl1 = New MyUserControl
myCustomTaskPane = Globals.ThisAddIn.CustomTaskPanes.Add(myUserControl1, "Test addin")
myCustomTaskPane.Width = 500
myCustomTaskPane.Visible = True
Dim uc As New UserControl1
' Check if there's a table
If isThereATable() Then
MsgBox("There is")
MsgBox(uc.Tab2.Header) ' works
uc.Tab2.Header = "new header" ' does not work
Else
MsgBox("There is not")
End If
End Sub
End Class
And this is the markup code:
<UserControl x:Class="UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:AutoTable"
mc:Ignorable="d"
d:DesignHeight="450" Width="500">
<Grid Width="500">
<TabControl BorderThickness="1" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" HorizontalAlignment="Left" Width="500" Name="MainTab">
<TabItem Header="Init" Name="Tab1" >
<Grid Background="#FFE5E5E5" Margin="0">
<TextBlock HorizontalAlignment="Left" TextWrapping="Wrap" Text="The following button should create a table." VerticalAlignment="Top" FontSize="16" Margin="10,10,0,0"/>
<Button Content="Create table" HorizontalAlignment="Left" Margin="5,60,0,0" VerticalAlignment="Top" Width="100" Click="Button_Click" Height="30" FontSize="14"/>
</Grid>
</TabItem>
<TabItem Header="CC" Name="Tab2" >
<Grid Background="#FFE5E5E5"/>
</TabItem>
</TabControl>
</Grid>
</UserControl>
I have called the tab control "MainTab", and the two tab items "Tab1" and "Tab2", I can not manipulate any of them with my current code.
UPDATE:
In order to use WPF controls in an Excel Add-In, the WPF User Control must be hosted by a Windows Forms UI Element, according to the docs. In this case, the WPF User Control is inside a Windows Forms User Control. The link above explains how this is done. In the code above, I was using variable names provided by microsoft's tutorial, but by using arguably better variable names, this piece of code would clarify the approach I was following and its solution:
Public Class Ribbon1
Private ThisUserControl As UserControlWF
Private ThisTaskPane As Microsoft.Office.Tools.CustomTaskPane
Private Sub ShowTaskPane_Click(sender As Object, e As RibbonControlEventArgs) Handles ShowTaskPane.Click
ThisUserControl = New UserControlWF
'CustomTaskPanes can only add Windows Forms UI elements, a WPF User Control throws an error
ThisTaskPane = Globals.ThisAddIn.CustomTaskPanes.Add(ThisUserControl, "Title")
ThisTaskPane.Width = 510
ThisTaskPane.Visible = True
' Just to test if a condition is true after it loads the task pane
If isThereATable() Then
MsgBox("There is a table")
ThisUserControl.UserControlWPF1.Tab2.Header = "Different Header" 'Original header name is "TabItem2"
ThisUserControl.UserControlWPF1.Tab2.IsSelected = True 'Tab1 is selected by default
' In my first approach, I was initializing another instance of the WPF control
' instead of using the one that was already initialized:
' Dim uc As New UserControlWPF
' MsgBox(uc.Tab2.Header) ' <- it worked
' uc.Tab2.Header = "Different Header" ' <- it did not work
' That's why I was able to access the Header from the class, but I couldn't
' modify the header that was already loaded
Else
MsgBox("There is no table")
End If
End Sub
End Class

You are setting the Header property of the TabItem in uc, which is a new control that you create without adding it to the add-in.
I guess you should set it in myUserControl1:
Private Sub btnCheck_Click(sender As Object, e As RibbonControlEventArgs) Handles btnCheck.Click
' Show the taskpane
myUserControl1 = New MyUserControl
myCustomTaskPane = Globals.ThisAddIn.CustomTaskPanes.Add(myUserControl1, "Test addin")
myCustomTaskPane.Width = 500
myCustomTaskPane.Visible = True
' Check if there's a table
If isThereATable() Then
MsgBox("There is")
MsgBox(myUserControl1.Tab2.Header) ' works
myUserControl1.Tab2.Header = "new header" ' does not work
Else
MsgBox("There is not")
End If
End Sub
Or you should add uc to Globals.ThisAddIn.CustomTaskPanes to be able to see the change.

Related

AddIn IRibbonUI callbacks and .Invalidate fails after it installs any other AddIn with its own IRibbonUI

Description: This is Excel 2013 VBA case. I created AddIn which should be able to install and uninstall another AddIns from AddIn list stored in global variable "listOfAddIns" which is Array of Arrays of Variant Type called "valueArray". There are "group control" buttons (for purpose of install or uninstall all available addins) and "individual control" toogle buttons (for install or uninstall specific AddIn). I use a lot of callback functions for purpose of full customization of all elements from VBA.
Problem: Everything works perfectly for installing/uninstalling AddIns without their own Ribbon Tabs. Once installed AddIn has its own Ribbon Tab, I am no longer able to use ribbon object .Invalidate method and call callbacks. I get this poorly descriped Error Message:
Run-Time error'-2147467259(80004005)': Method 'Invalidate' of object 'IRibbonUI' failed
My Idea: Due to described symptomns, I suspect, that there are some duplicity problems of IRibbonControl from different AddIns. Despite a lot of efforts, I cannot figure it out.
Relevant part of CustomUI XML:
<customUI onLoad="OnLoad" xmlns="http://schemas.microsoft.com/office/2006/01/customui">
<ribbon startFromScratch="false">
<tabs>
<tab id = "INST1ID" label="Installer">
<group id="GroupControls" label="Group controls">
<button id="B1ID" label="Install All" size="large" onAction="InstallAll" imageMso="AcceptInvitation" />
<button id="B2ID" label="Uninstall ALL" size="large" onAction="UninstallAll" imageMso="MasterViewClose" />
</group>
<group id="IndividualControls" label="Individual controls">
<toggleButton id="TB1ID" imageMso="HappyFace" label="AddIn1" onAction="TBsControl" size="large" tag="1" getEnabled="TBsGetEnabled" getPressed="TBsGetPressed" getVisible="TBsGetVisible" />
<toggleButton id="TB2ID" imageMso="HappyFace" label="AddIn2" onAction="TBsControl" size="large" tag="2" getEnabled="TBsGetEnabled" getPressed="TBsGetPressed" getVisible="TBsGetVisible" />
<toggleButton id="TB3ID" imageMso="HappyFace" label="AddIn3" onAction="TBsControl" size="large" tag="3" getEnabled="TBsGetEnabled" getPressed="TBsGetPressed" getVisible="TBsGetVisible" />
</group>
</tab>
</tabs>
</ribbon>
</customUI>
OnLoad Sub:
Public Sub OnLoad(ribbon As IRibbonUI)
Set ribbonObject = ribbon
End Sub
Main procedure for "Group control" example:
Public Sub InstallAll(control As IRibbonControl)
Call ResetSomePublicVariables
Call CreateEntryValuesAndFillToListOfAddIns
Call CheckAvailabilityAndInstallationOfAddInsAndFillToListOfAddIns
Call UpdateListOfAddInsToInstallAllAvailableAddIns
Call InstallOrUninstallAddInsDueToListOfAddIns
ribbonObject.Invalidate
End Sub
Main procedure for "Individual control" and expanded 4th Sub:
Public Sub TBsControl(control As IRibbonControl, pressed As Boolean)
Call ResetSomePublicVariables
Call CreateEntryValuesAndFillToListOfAddIns
Call CheckAvailabilityAndInstallationOfAddInsAndFillToListOfAddIns
Call UpdateListOfAddInsDueToToogleButtonRequest(control.tag, pressed)
Call InstallOrUninstallAddInsDueToListOfAddIns
ribbonObject.Invalidate
End Sub
Public Sub UpdateListOfAddInsDueToToogleButtonRequest(tag As Long, pressed As Boolean)
Dim valueArray() As Variant
valueArray = listOfAddIns(tag)
If pressed = True Then
valueArray(VAColumnIndex_Installed) = 1
Else
valueArray(VAColumnIndex_Installed) = 0
End If
listOfAddIns(tag) = valueArray
End Sub
Callback example:
Public Sub TBsGetVisible(control As IRibbonControl, ByRef returnedVal)
For Each Item In listOfAddIns
If control.tag = Item(VAColumnIndex_Number) Then
returnedVal = True
Exit For
Else
returnedVal = False
End If
Next Item
End Sub
I didnt used Custom UI Editor or similar software and I guess that it will not be helpful in this phase
I tried to move onLoad sub to ThisWorkbook module. I understand, that moving callbacks to ThisWorkbook is wrong way. I also tried ThisWorkbook and ThisAddin prefixes to IRibbonControl argument, but thats also wrong way. So far with my tries to somehow differentiate eventual IRibbonControl duplicities (my idea only).
I tried to rename IRibbonUI objects in other AddIns without any progress.
I read a lot of sources and saw a lot of examples and different approaches on Microsoft, StackOverFlow on link below but it didnt helped me with this.
https://excelguru.ca/?s=ribbon+part
Any ideas and especially simple solutions of this problem are very welcomed, thank you very much
Solved.
First line of XML file of all my AddIns looked like this and that was a source of all problems described before.
<customUI onLoad="OnLoad" xmlns="http://schemas.microsoft.com/office/2006/01/customui">
Same name for onLoad procedure in different AddIns resulted in described behaviour including earlier mentioned error when 2 or more AddIns with this same name was installed. Renaming onLoad procedures to different names solved everything instantly.
Source which helped me: https://excelguru.ca/debugging-ribbonx-invalidateinvalidatecontrol-failures%E2%80%A6/

Excel VBA Buttonbar on sheet

I'm trying to find a ButtonBar solution to add to a sheet in excel (so not in forms, directly on the sheet)
While looking I ran into the ActivX controll: ButtonBar Class, that gets added like the code below.
Can anyone tell me how I can add buttons to this control?
Or do you know of any other buttonbar bype controls I could use on an Exel sheet?
ActiveSheet.OLEObjects.Add(ClassType:="UmOutlookAddin.ButtonBar.1", Link:= _
False, DisplayAsIcon:=False, Left:=96.75, Top:=15, Width:=214.5, _
Height:=17.25).Select
You can control the click unsing the code below, but I have not found a way to add new buttons:
Private Sub ButtonBar1_OnClick(ByVal ButtonId As Long)
I don't think you can add buttons. I tried changing the label and that crashed Excel:
Sub Test()
Dim bb As ButtonBar
Set bb = ActiveSheet.OLEObjects(1).Object
bb.SetButtonLabel PlayButtonId, "Test" 'Boom
End Sub
The ButtonBar seems too unstable and I would not recommend using it.
However, you have other options. For example, on the Developer tab you have the simple Button control:
You can add multiple buttons and then group them:
You could obviously make them adjacent to mimic a bar:
As you can see, they even have a 'pressed' animation when you click them (mid button).
If you don't need the animation then you can just add any shape to work as a button. You would add one shape and format it and then make copies and assign a different macro for each (with right click and Assign Macro...). You would then group them when done. For example:
Or, you could just use a custom ribbon tab if you don't necessarily need the buttons in the sheet itself. Here is an example where I showed step by step how to add a custom ribbon but there are many ways of doing it if you search the web. In that example the custom ribbon is not used to display anything but rather is used for it's Init event. But it's easy to replace the xml at step 2f with something like this:
<customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui" onLoad="InitRibbon">
<ribbon>
<tabs>
<tab id ="TestTabID" Label="Test">
<group id="FirstGroupID" Label="First Group">
<button id="RefreshData" label="Refresh Data" size="large" imageMso="Refresh" onAction="RibbonCallTool" />
<button id="UnloadData" label="Unload Data" size="large" imageMso="RecordsDeleteRecord" onAction="RibbonCallTool" />
</group>
</tab>
</tabs>
</ribbon>
</customUI>
in which case you would also have this method in a standard VBA module:
'*******************************************************************************
'Callback ("onAction"). Runs when a control is clicked in the Custom Ribbon tab
'*******************************************************************************
Public Sub RibbonCallTool(ByVal ctrl As IRibbonControl)
Select Case ctrl.ID
Case "RefreshData"
MsgBox "Refresh"
Case "UnloadData"
MsgBox "Unload"
Case Else
Debug.Print "Control <" & ctrl.ID & "> does not have an associated action attached!"
End Select
End Sub
Finally, you could always have a single button that opens a modeless form with all the menus you need.

VBA Enable Ribbon button by Selection

I am trying to figure out how I can enable a ribbon button based on Selection, I know I need to use Worksheet_SelectionChange Event however I not sure how to proceed. I have exhausted all options looking on how to do it, can someone help with this please, I have asked on Mr Excel but no answers to what I need.
Example of what I am looking for is:
If a column is selected then enable ribbon button a or If a Row is selected then Enable ribbon button b
Use ribbon callbacks and call IRibbonUI.Invalidate or IRibbonUI.InvalidateControl where appropriate. See How to get the reference to the IRibbonUI in VBA? for more information.
You can customize the Ribbon UI by using callback procedures in VBA macros or COM add-ins. For each of the callbacks the add-in implements, the responses are cached. For example, if an add-in writer implements the getImage callback procedure for a button, the function is called once, the image loads, and then if the image needs to be updated, the cached image is used instead of recalling the procedure. This process remains in-place until the code signals that the cached values are invalid by using the Invalidate method, at which time, the callback procedure is again called and the return response is cached. The add-in or VBA macros can then force an immediate update of the UI by calling the Refresh method.
In your custom UI XML file you need to declare the onLoad callback:
<customUI … onLoad=”MyAddInInitialize” …>
And then in VBA you could use:
Dim MyRibbon As IRibbonUI
Sub MyAddInInitialize(Ribbon As IRibbonUI)
Set MyRibbon = Ribbon
End Sub
Sub myFunction()
‘ Invalidates the caches of all of this add-in’s controls
MyRibbon.Invalidate()
End Sub
Read more about the Fluent UI (aka Ribbon UI) in the following articles:
Customizing the 2007 Office Fluent Ribbon for Developers (Part 1 of 3)
Customizing the 2007 Office Fluent Ribbon for Developers (Part 2 of 3)
Customizing the 2007 Office Fluent Ribbon for Developers (Part 3 of 3)
This is what I have so far but I'm still a novice.
<customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui" onLoad="OnRibbonLoad">
<ribbon>
<tabs>
<tab id="MyTools" label="Tools">
<group id="MoveGroup" label="Move" tag="GroupMove" >
<button id="MoveColumnLeft" tag="EnableLeft" imageMso="GoRtl" screentip="Move Column Left" supertip="Move the selected column to the left" label="Left" size="large" onAction="OnActionButton" getEnabled="GetEnabled" />
<button id="MoveColumnRight" tag="EnableRight" imageMso="GoLeftToRight" screentip="Move Column Right" supertip="Move the selected column to the right" label="Right" size="large" onAction="OnActionButton" getEnabled="GetEnabled" />
`<separator id="MoveSep" />`
<button id="MoveRowUp" tag="EnableUp" imageMso="MessagePrevious" screentip="Move Row Up" supertip="Move the selected row up" label="Up" size="large" onAction="OnActionButton" getEnabled="GetEnabled" />
<button id="MoveRowDown" tag="EnableDown" imageMso="MessageNext" screentip="Move Row Down" supertip="Move the selected row down" label="Down" size="large" onAction="OnActionButton" getEnabled="GetEnabled" />
</group>
</tab>
</tabs>
</ribbon>
</customUI>
Option Explicit
Public oRibbon As IRibbonUI, bEnabled As Boolean
Sub OnRibbonLoad(ribbon As IRibbonUI)
Set oRibbon = ribbon
bEnabled = True
End Sub
Sub OnActionButton(control As IRibbonControl)
Select Case control.ID
Case "MoveColumnLeft"
bEnabled = enabled
oRibbon.Invalidate
Case "MoveColumnRight"
bEnabled = enabled
oRibbon.Invalidate
End Select
End Sub
Sub GetEnabled(control As IRibbonControl, ByRef enabled)
`Select Case control.ID
Case "MoveColumnLeft"
enabled = enabled
Case "MoveColumnRight"
enabled = enabled
End Select
End Sub

Calling an excel macro from the ribbon

Intro:
I have written some short excel macros (tested, they work fine) and want to link them to a button in the Ribbon (Excel 2010). I had already done it successfully in Excel 2007.
I am using Custom UI Editor to build a new ribbon, which also works fine. Everything is packaged in a .xlam add-in and added to Excel. The ribbon shows up nicely, all other buttons works, but ...
Problem:
when I hit the button that is linked to the macro I get the error: "wrong number of parameters or property assignment not valid" (message translated from Italian, might not be exactly the same in English)
Troubleshooting info:
The macros do not have parameters. The same macros can be successfully called and executed manually. I am even able to add the same macros to the Quick Access Toolbar.
Here is the specific portion of the ribbon script:
<group id="DupNumber" label="Number" insertBeforeMso="GroupNumber" >
<comboBox idMso="NumberFormatGallery"/>
<box id="HN1" boxStyle="horizontal">
<buttonGroup id="HNButtonGroup1">
<button id="Euro" onAction="Roberto.xlam!EURZ" imageMso="F" supertip="text ..."/>
<button id="EuroNZ" onAction="Roberto.xlam!EURNZ" imageMso="E" supertip="text ..."/>
<button idMso="PercentStyle"/>
<button id="Comma" onAction="Roberto.xlam!NewCommaFormat" imageMso="C" supertip="test ..."/>
<button idMso="PercentStyle"/>
</buttonGroup>
</box>
and here are the macros:
Sub EURZ()
Application.ActiveCell.NumberFormat = "€ #,##0.00"
End Sub
Sub EURNZ()
Application.ActiveCell.NumberFormat = "€ #,##0"
End Sub
Sub NewCommaFormat()
Application.ActiveCell.NumberFormat = "#,##0"
End Sub
Can you help me?
Thanks
Roberto
I believe you need to add this param to your macro: control As IRibbonControl
So it should look like this:
Sub EURZ(control As IRibbonControl)
Application.ActiveCell.NumberFormat = "€ #,##0.00"
End Sub

How to override Copy command on WPF textbox?

I would like to override the behavior of RoutedUICommand "Copy" of a WPF TextBox.
Is it possible without creating a new TextBoxExtended class that inherits from TextBox?
I have reached that point, but now I am bit lost.
Private Sub tbSource_PreviewExecuted(ByVal sender As System.Object, ByVal e As System.Windows.Input.ExecutedRoutedEventArgs)
Dim commandName = DirectCast(e.Command, Input.RoutedUICommand).Text
If commandName = "Copy" Then
End If
End Sub
Do you have any idea how to continue?
You can add a command binding to the text box to handle the "Copy" command. For example, like this:
<StackPanel>
<TextBlock x:Name="TextBox">
<TextBlock.CommandBindings>
<CommandBinding Command="{x:Static ApplicationCommands.Copy}"
Executed="CommandBinding_Executed"/>
</TextBlock.CommandBindings>
</TextBlock>
<Button Content="Copy"
Command="{x:Static ApplicationCommands.Copy}"
CommandTarget="{Binding ElementName=TextBox}"/>
</StackPanel>

Resources