I raised a query which now works as per David Zemens' instructions and BrakNicku guidance.
Problem is one of the events I want to use is Enter. Within the class, I don't get the option for this event. Is there is a way to add this to the class or trigger an Enter event for the control somehow?
I tried most of the events available within the class but none of them behave the way I need them to.
A quick background: I use Enter event to set help text for the field in focus. So every time a user enters a field, I have a help textbox that gets populated with help text.
I am unable to share the workbook.
Let's say your userform (Userform1) looks like this
I am going to demonstrate the Enter Event for 2 controls. TextBox and ComboBox.
Ensure that you place the CommandButton1 first on the userform. Or alternatively, set it's TabIndex to 0. This is so that the command button takes focus first when the userform loads and you can test the Entering of TextBox and ComboBox.
Paste this in a class module. My Class module name is Class1
Option Explicit
Public WithEvents Usrfrm As UserForm1
Const MyMsg As String = "Hiya there. Did you just try to sneak into the "
Private Sub Usrfrm_OnEnter(ctrl As msforms.Control)
Select Case True
Case TypeName(ctrl) Like "ComboBox"
'Call Usrfrm.Combobox_List(ctrl)
MsgBox MyMsg & "combobox?", vbCritical, "Aha!"
Case TypeName(ctrl) Like "TextBox"
MsgBox MyMsg & "textbox?", vbCritical, "Aha!"
End Select
End Sub
Paste this in the userform code area
Option Explicit
Public Event OnEnter(ctrl As msforms.Control)
Private prevCtl As msforms.Control
Private mycls As Class1
Private IsfrmUnloaded As Boolean
Private Sub CommandButton1_Click()
Unload Me
End Sub
Private Sub UserForm_Layout()
Call spyWhatsGoingOn
End Sub
Private Sub spyWhatsGoingOn()
Set mycls = New Class1
Set mycls.Usrfrm = Me
IsfrmUnloaded = False
Set prevCtl = Me.ActiveControl
RaiseEvent OnEnter(Me.ActiveControl)
Do While IsfrmUnloaded = False
If Not prevCtl Is Nothing Then
If Not prevCtl Is Me.ActiveControl Then
RaiseEvent OnEnter(Me.ActiveControl)
Me.ActiveControl.SetFocus
End If
End If
Set prevCtl = Me.ActiveControl
DoEvents
Loop
End Sub
Demo
Here another solution, (doesnot work on a MAC)
Open Notepad and copy code below and paste it in a new txt-file
save it als CatchEvents.cls
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "CatchEvents"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Private Type GUID
Data1 As Long
Data2 As Integer
Data3 As Integer
Data4(0 To 7) As Byte
End Type
#If VBA7 And Win64 Then
Private Declare PtrSafe Function ConnectToConnectionPoint Lib "shlwapi" Alias "#168" (ByVal punk As stdole.IUnknown, _
ByRef riidEvent As GUID, ByVal fConnect As Long, ByVal punkTarget As stdole.IUnknown, ByRef pdwCookie As Long, _
Optional ByVal ppcpOut As LongPtr) As Long
#Else
Private Declare Function ConnectToConnectionPoint Lib "shlwapi" Alias "#168" (ByVal punk As stdole.IUnknown, ByRef riidEvent As GUID, _
ByVal fConnect As Long, ByVal punkTarget As stdole.IUnknown, ByRef pdwCookie As Long, Optional ByVal ppcpOut As Long) As Long
#End If
Private EventGuide As GUID
Private Ck As Long
Private ctl As Object
'All Other Control-Events also possible
Public Sub MyEnter()
Attribute MyEnter.VB_UserMemId = -2147384830
Select Case TypeName(ctl)
Case "TextBox": MsgBox "Your code for " & ctl.Name & " here!"
Case Else: MsgBox "You entered no TextBox but another control (" & ctl.Name & ")!"
End Select
End Sub
Public Sub ConnectAllEvents(ByVal Connect As Boolean)
With EventGuide
.Data1 = &H20400
.Data4(0) = &HC0
.Data4(7) = &H46
End With
ConnectToConnectionPoint Me, EventGuide, Connect, ctl, Ck, 0&
End Sub
Public Property Let Item(Ctrl As Object)
Set ctl = Ctrl
Call ConnectAllEvents(True)
End Property
Public Sub Clear()
If (Ck <> 0) Then Call ConnectAllEvents(False)
Set ctl = Nothing
End Sub
In your VBA editor you import this File
In your Userform code you add:(when you have already an Initialize-event you combine those)
Private AllControls() As New CatchEvents 'on top
Private Sub UserForm_Initialize()
ReDim AllControls(Controls.Count - 1)
For j = 0 To Controls.Count - 1
AllControls(j).Item = Controls(j)
Next
End Sub
Now every Enter-event of any control will be catched, so you have to act accordingly.
Every event on a Userform can be catched this way.
So the approach I went with was: I already had Class Module that was trapping Change event (can be seen here). As i didnt have access to Enter event in my class, I used the KeyUp and MouseDown events in this class to set help for each control. This way user can get to a field by clicking on it or tabbing to it: help is displayed for the selected control
Related
Idea is to have a ComboBox open - when mouse cursor moves over it, without having to click on it, and when mouse moves off it for it to close back down.
Code below seem to be opening it, but with caveats:
a) code keeps firing the Alt + Arrow Down while you are thinking what list item to pick - want it to do it just to open the ComboBox and then stop,
b) once you pick the item, it fires Alt + Arrow Down on a new active cell, which is not the intent. And that new cell prevents ComboBox from opening on the next mouse over.
(*I also have another code, which once list item is picked is finding and selecting corresponding cell in A column)
Private Sub ComboBox1_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
ComboBox1.Activate
SendKeys "%{DOWN}"
End Sub
Some users might find move over solution annoying -- but would be interesting to test it out, if it comes along.
Went with a click anywhere on the Box meantime, as it is better than having to click on an arrow icon only.
Private Sub ComboBox1_GotFocus() 'when clicked on anywhere - not just arrow icon, opens
SendKeys "%{DOWN}"
End Sub
on your form create TextBox1 and ListBox1.
Paste this example code into your form.
Dim autoList As CautoOpenList
Private Sub TextBox1_Enter()
'optional capture of keyboard action to leave textbox
autoList.flipFields True
End Sub
Private Sub ListBox1_Exit(ByVal Cancel As MSForms.ReturnBoolean)
' optional capture of keyboard action to leave listbox
autoList.flipFields False
End Sub
Private Sub UserForm_Initialize()
ListBox1.List = split("A,B,C,D,E,F",",") ' value your listbox with your values
Set autoList = New CautoOpenList
autoList.Form = Me
autoList.LiBox = ListBox1 ' your listbox name
autoList.TxBox = TextBox1 'your textbox name
End Sub
Then include this class module into your workbook with the name CautoOpenList.
Option Explicit
Private taboff As Boolean, aForm As MSForms.UserForm
Private WithEvents LiBx As MSForms.ListBox
Private WithEvents TxBx As MSForms.TextBox
Private WithEvents Labl As MSForms.Label
Public Property Let LiBox(obj As Object)
Set LiBx = obj
LiBx.Visible = False
End Property
Public Property Let TxBox(obj As Object)
' connect listbox before connecting text box
Set TxBx = obj
With LiBx
.Top = TxBx.Top - 5
.Left = TxBx.Left - 5
End With
'label necessary to capture mouse off-to-right when listbox has scroll bar
Set Labl = aForm.Controls.Add("forms.label.1", "Labl", False)
With Labl
.Top = LiBx.Top
.Height = LiBx.Height
.Width = 5
.Left = LiBx.Left + LiBx.Width
End With
End Property
Public Property Let Form(obj As Object)
' connect from to class before connecting the textbox
Set aForm = obj
End Property
Private Sub LiBx_Click()
TxBx.Text = LiBx.Text
End Sub
Private Sub Labl_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
flipFields False
Debug.Print "MM"
End Sub
Private Sub LiBx_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
With LiBx
If X < 4 Then ' Or X > nRightBound Then
flipFields False
End If
If Y < 4 Or Y > .Height - 6 Then
flipFields False
End If
End With
End Sub
Private Sub TxBx_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
With LiBx
flipFields True
End With
End Sub
Public Sub flipFields(b As Boolean)
LiBx.Visible = b
Labl.Visible = b
TxBx.Visible = Not b
DoEvents
End Sub
Having a procedure c1(), that looks like this:
Sub c1(control As IRibbonControl)
Debug.Print "foo"
End Sub
it is being called successfully from the ribbon, with the correct code onAction="c1"/>
Question:
How to call the same procedure from the immediate window (for debugging purposes)? It is asking for a parameter, which I do not know how to provide:
If your code in c1 does not rely on the control you can use
c1 nothing
in the immediate window.
What you can do for example:
Private lobjRibbon As IRibbonUI
Public Declare PtrSafe Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" ( _
ByRef Destination As Any, ByRef source As Any, ByVal length As Long)
Public Sub Ribbon_CallbackOnLoad(ByRef probjRibbon As IRibbonUI)
Set lobjRibbon = probjRibbon
Range("A1").Value = CStr(ObjPtr(lobjRibbon)) 'write the pointer to a cell to save it even if VBA stops
End Sub
with Ribbon_CallbackOnLoad you set the ribbon on load to the variable lobjRibbon or save it's pointer (in case VBA completely ends).
You can use
Function GetRibbon(ByVal lRibbonPointer As LongPtr) As Object
Dim objRibbon As Object
Call CopyMemory(objRibbon, lRibbonPointer, LenB(lRibbonPointer))
Set GetRibbon = objRibbon
Set objRibbon = Nothing
End Function
to get the ribbob object back from its saved pointer for example like in
Sub RefreshRibbon(Optional ControlID As String = vbNullString)
If lobjRibbon Is Nothing Then
Set lobjRibbon = GetRibbon(CLngPtr(Range("A1").Value))
If ControlID = vbNullString Then
lobjRibbon.Invalidate
Else
lobjRibbon.InvalidateControl ControlID
End If
Else
If ControlID = vbNullString Then
lobjRibbon.Invalidate
Else
lobjRibbon.InvalidateControl ControlID
End If
End If
End Sub
Try this workaround
Sub c1(control As IRibbonControl)
call c1_subroutine
End Sub
Sub c1_subroutine()
Debug.Print "foo"
End Sub
And in the immediate window call c1_subroutine
I've got a modeless userform in an Excel VBA project.
The userform is loaded by button on spreadsheet clicked (not an active-x button if that's relevant).
Because of modeless the user can work with excel or even other applications and than switch back to the form window. I need an event that fires if the form window becomes the active window again. I thought UserForm_Activate should do the job but it doesn't (neither does UserForm_GotFocus but there is no GotFocus event for userforms?). Is there any event that fires if the user switches back to a modeless userform (or in case not: is there any known work-around)? Or do I've got some strange bug here and Activate should fire?
Here's all the code I used for testing purpose:
' standard module:
Sub BUTTON_FormLoad()
' associated as macro triggered by button click on a sheet
UserForm1.Show vbModeless
End Sub
' UserForm1:
Private Sub UserForm_Activate()
' does not fire if focus comes back
Debug.Print "Activated"
End Sub
Private Sub UserForm_GotFocus()
' does not fire if focus comes back
' wrong code - no GotFocus event for userforms?
Debug.Print "Focussed"
End Sub
Private Sub UserForm_Click()
' only fires if clicked *inside* form
' does not fire eg if user clicks top of form window
Debug.Print "Clicked"
End Sub
Where do I find the documentation of userform events? It's not on the 'UserForm object' page.
The Activate event doesn't fire when you switch between the application and a modeless userform. This is by design.
Like I mentioned in the comments
You can achieve what you want by subclassing the userform and trapping the worksheet events but it very messy.
Here is a very basic example. Sample file can be downloaded from Here
READ ME FIRST:
This is just a basic sample. Please close all Excel Files before testing this.
If the user directly clicks a control on the userform and you want to run the activate code there as well then you will have to handle that as well.
Once you are happy, amend it to suit your need.
Place code in a Module
Option Explicit
Private Declare Function CallWindowProc Lib "user32" Alias "CallWindowProcA" _
(ByVal lpPrevWndFunc As Long, ByVal hwnd As Long, ByVal msg As Long, _
ByVal wParam As Long, ByVal lParam As Long) As Long
Private Declare Function SetWindowLong& Lib "user32" Alias "SetWindowLongA" _
(ByVal hwnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long)
Private Const GWL_WNDPROC = (-4)
Private WinProcOld As Long
Private Const WM_NCLBUTTONDOWN = &HA1
Public formWasDeactivated As Boolean
'~~> Launch the form
Sub LaunchMyForm()
Dim frm As New UserForm1
frm.Show vbModeless
End Sub
'~~> Hooking the Title bar in case user clicks on the title bar
'~~> to activate the form
Public Function WinProc(ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
If wMsg = WM_NCLBUTTONDOWN Then
'~~> Ignoring unnecessary clicks to the title bar
'~~> by checking if the form was deactivated
If formWasDeactivated = True Then
formWasDeactivated = False
MsgBox "Form Activated"
End If
End If
WinProc = CallWindowProc(WinProcOld&, hwnd&, wMsg&, wParam&, lParam&)
End Function
'~~> Subclass the form
Sub SubClassUserform(hwnd As Long)
WinProcOld& = SetWindowLong(hwnd, GWL_WNDPROC, AddressOf WinProc)
End Sub
Sub UnSubClassUserform(hwnd As Long)
SetWindowLong hwnd, GWL_WNDPROC, WinProcOld&
WinProcOld& = 0
End Sub
Create a Userform. Let's call it Userform1. Add a command button in the form. Let's call it CommandButton1
Place code in Userform
Option Explicit
Private Declare Function FindWindow Lib "user32.dll" _
Alias "FindWindowA" (ByVal lpClassName As String, _
ByVal lpWindowName As String) As Long
Dim hwnd As Long
Private Sub UserForm_Initialize()
hwnd = FindWindow(vbNullString, Me.Caption)
SubClassUserform hwnd
End Sub
'~~> Userform Click event
Private Sub UserForm_Click()
'~~> Ignoring unnecessary clicks
'~~> by checking if the form was deactivated
If formWasDeactivated = True Then
formWasDeactivated = False
MsgBox "Form Activated"
End If
End Sub
'~~> Unload the form
Private Sub CommandButton1_Click()
'~~> In case hwnd gets reset for whatever reason.
hwnd = FindWindow(vbNullString, Me.Caption)
UnSubClassUserform hwnd
Unload Me
End Sub
Place this code in the Workbook code area
Option Explicit
'~~> Checking if the form was deactivated
'~~> Add more events if you want
Private Sub Workbook_SheetActivate(ByVal Sh As Object)
formWasDeactivated = True
End Sub
Private Sub Workbook_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range)
formWasDeactivated = True
End Sub
Please feel free to add more workbook events. I have only used Workbook_SheetActivate and Workbook_SheetSelectionChange
And finally add a Form Button in the worksheet and assign the macro LaunchMyForm to it. And we are done
In Action
As far as I know, there isn't such an event within VBA. From the documentation:
The Activate and Deactivate events occur only when you move the focus
within an application. Moving the focus to or from an object in
another application doesn't trigger either event.
However, the Windows APIs can handle the event with a hook. The problem with Win APIs within VBA is that errors aren't handled by VBA so Excel will crash if/when the code encounters an error; so they can be frustrating for the developer. From a purely personal perspective, I like to keep code within the hook procedures to a minimum and pass any values to a class that can then fire events - this at least minimises the crashes. It's also important to remember to unhook before finishing your session.
Basic implementation of a Win API hook would look something like this:
In a class object (here it's called cHookHandler)
Option Explicit
Public Event HookWindowActivated()
Public Event HookIdChanged()
Private mHookId As LongPtr
Private mTargetWindows As Collection
Public Property Get HookID() As LongPtr
HookID = mHookId
End Property
Public Property Let HookID(RHS As LongPtr)
mHookId = RHS
RaiseEvent HookIdChanged
End Property
Public Sub AttachHook()
modHook.AttachHook Me
End Sub
Public Sub DetachHook()
modHook.DetachHook
End Sub
Public Sub AddTargetWindow(className As String, Optional windowTitle As String)
Dim v(1) As String
'Creates an array of [0 => className, 1=> windowTitle]
'which is stored in a collection and tested for in
'your hook callback.
v(0) = className
v(1) = windowTitle
mTargetWindows.Add v
End Sub
Public Sub TestForTargetWindowActivated(className As String, windowTitle As String)
Dim v As Variant
'Tests if the callback window is one that we're after.
For Each v In mTargetWindows
If v(0) = className Then
If v(1) = "" Or v(1) = windowTitle Then
'Fires the event that our target window has been activated.
RaiseEvent HookWindowActivated
Exit Sub
End If
End If
Next
End Sub
Private Sub Class_Initialize()
Set mTargetWindows = New Collection
End Sub
Private Sub Class_Terminate()
modHook.DetachHook
End Sub
Module code (here the module is called modHook)
Option Explicit
Private Declare PtrSafe Function UnhookWindowsHookEx Lib "user32" _
(ByVal hHook As LongPtr) As Long
Private Declare PtrSafe Function SetWindowsHookEx Lib "user32" _
Alias "SetWindowsHookExA" _
(ByVal idHook As Long, _
ByVal lpfn As LongPtr, _
ByVal hmod As LongPtr, _
ByVal dwThreadId As Long) As LongPtr
Private Declare PtrSafe Function CallNextHookEx Lib "user32" _
(ByVal hHook As LongPtr, _
ByVal ncode As Long, _
ByVal wParam As LongPtr, _
lParam As Any) As LongPtr
Private Declare PtrSafe Function GetClassName Lib "user32" _
Alias "GetClassNameA" _
(ByVal hwnd As LongPtr, _
ByVal lpClassName As String, _
ByVal nMaxCount As Long) As Long
Private Declare PtrSafe Function GetWindowText Lib "user32" _
Alias "GetWindowTextA" _
(ByVal hwnd As LongPtr, _
ByVal lpString As String, _
ByVal cch As Long) As Long
Private Declare PtrSafe Function GetCurrentThreadId Lib "kernel32" () As Long
Private Const WH_CBT As Long = 5
Private Const HCBT_ACTIVATE As Long = 5
Private mHookHandler As cHookHandler
Public Sub AttachHook(hookHandler As cHookHandler)
Set mHookHandler = hookHandler
mHookHandler.HookID = SetWindowsHookEx(WH_CBT, AddressOf CBTCallback, 0, GetCurrentThreadId)
End Sub
Private Function CBTCallback(ByVal lMsg As Long, _
ByVal wParam As LongPtr, _
ByVal lParam As LongPtr) As LongPtr
Dim className As String, windowTitle As String
If mHookHandler Is Nothing Then Exit Function
If lMsg = HCBT_ACTIVATE Then
className = GetClassText(wParam)
windowTitle = GetWindowTitle(wParam)
If Not mHookHandler Is Nothing Then
mHookHandler.TestForTargetWindowActivated className, windowTitle
End If
End If
CBTCallback = CallNextHookEx(mHookHandler.HookID, lMsg, ByVal wParam, ByVal lParam)
End Function
Public Sub DetachHook()
Dim ret As Long
If mHookHandler Is Nothing Then Exit Sub
ret = UnhookWindowsHookEx(mHookHandler.HookID)
If ret = 1 Then
mHookHandler.HookID = 0
End If
End Sub
Private Function GetWindowTitle(wParam As LongPtr) As String
Dim tWnd As String
Dim lWnd As Long
tWnd = String(100, Chr(0))
lWnd = GetWindowText(wParam, tWnd, 100)
tWnd = Left(tWnd, lWnd)
GetWindowTitle = tWnd
End Function
Private Function GetClassText(wParam As LongPtr) As String
Dim tWnd As String
Dim lWnd As Long
tWnd = String(100, Chr(0))
lWnd = GetClassName(wParam, tWnd, 100)
tWnd = Left(tWnd, lWnd)
GetClassText = tWnd
End Function
And in this example, all events are captured within the Userform
In this simple example, two buttons on the Userform attach and detach the hook, but you'd probably call the routines from somewhere else (perhaps the userform Initialize and Terminate events). The Userform also has a label lblHook displaying the HookId which I use during development - for production code, you probably wouldn't want this, so you could leave that bit out.
Option Explicit
Private WithEvents mHookHandler As cHookHandler
Private Sub btnHook_Click()
mHookHandler.AttachHook
End Sub
Private Sub btnUnhook_Click()
mHookHandler.DetachHook
End Sub
Private Sub mHookHandler_HookIdChanged()
lblHook.Caption = mHookHandler.HookID
End Sub
Private Sub mHookHandler_HookWindowActivated()
' Caveat: this routine will crash if halted in debugger.
Debug.Print "I've been activated!"
End Sub
Private Sub UserForm_Initialize()
Set mHookHandler = New cHookHandler
mHookHandler.AddTargetWindow "ThunderDFrame", Me.Caption
End Sub
Private Sub UserForm_Terminate()
Set mHookHandler = Nothing
End Sub
Try this. the event occurs after the form appears, so hide the wb inside an initialize event.
Private Sub UserForm_Initialize()
Set WB = ThisWorkbook Windows(WB.Name).Visible = False
The event does not exist and you can use Windows hooks to achieve your desired result. In my opinion, that's the direct answer and everything else is a workaround [unless it was posted by Siddharth Rout, in which case, THAT is the direct answer]
Creating a contextual menu within an Excel User form that applies to Images...
I am trying to write a piece of VBA code to allow me to use a contextual menu generated from right clicking an Image on an Excel User form.
Andy Pope kindly gave the world a great bit of code to add a simple context menu that applies to textboxes within an Excel User form, but not Userform.Images.
http://www.andypope.info/vba/uf_contextualmenu.htm
I have edited his code ever so slightly to prevent the contextual usage of Locked = True textboxes.
'Copyright ©2007-2014 Andy Pope
Option Explicit
Private Const mEDIT_CONTEXTMENU_NAME = "ajpiEditContextMenu"
Private Const mCUT_TAG = "CUT"
Private Const mCOPY_TAG = "COPY"
Private Const mPASTE_TAG = "PASTE"
Private m_cbrContextMenu As CommandBar
Private WithEvents m_txtTBox As MSForms.TextBox
Private WithEvents m_cbtCut As CommandBarButton
Private WithEvents m_cbtCopy As CommandBarButton
Private WithEvents m_cbtPaste As CommandBarButton
Private m_objDataObject As DataObject
Private m_objParent As Object
Private Function m_CreateEditContextMenu() As CommandBar
'
' Build Context menu controls.
'
Dim cbrTemp As CommandBar
Const CUT_MENUID = 21
Const COPY_MENUID = 19
Const PASTE_MENUID = 22
Set cbrTemp = Application.CommandBars.Add(mEDIT_CONTEXTMENU_NAME, Position:=msoBarPopup)
With cbrTemp
With .Controls.Add(msoControlButton)
.Caption = "Cu&t"
.FaceId = CUT_MENUID
.Tag = mCUT_TAG
End With
With .Controls.Add(msoControlButton)
.Caption = "&Copy"
.FaceId = COPY_MENUID
.Tag = mCOPY_TAG
End With
With .Controls.Add(msoControlButton)
.Caption = "&Paste"
.FaceId = PASTE_MENUID
.Tag = mPASTE_TAG
End With
End With
Set m_CreateEditContextMenu = cbrTemp
End Function
Private Sub m_DestroyEditContextMenu()
On Error Resume Next
Application.CommandBars(mEDIT_CONTEXTMENU_NAME).Delete
Exit Sub
End Sub
Private Function m_GetEditContextMenu() As CommandBar
On Error Resume Next
Set m_GetEditContextMenu = Application.CommandBars(mEDIT_CONTEXTMENU_NAME)
If m_GetEditContextMenu Is Nothing Then
Set m_GetEditContextMenu = m_CreateEditContextMenu
End If
Exit Function
End Function
Private Function m_ActiveTextbox() As Boolean
'
' Make sure this instance is connected to active control
' May need to drill down through container controls to
' reach ActiveControl object
'
Dim objCtl As Object
Set objCtl = m_objParent.ActiveControl
Do While UCase(TypeName(objCtl)) <> "TEXTBOX"
If UCase(TypeName(objCtl)) = "MULTIPAGE" Then
Set objCtl = objCtl.Pages(objCtl.Value).ActiveControl
Else
Set objCtl = objCtl.ActiveControl
End If
Loop
m_ActiveTextbox = (StrComp(objCtl.Name, m_txtTBox.Name, vbTextCompare) = 0)
ErrActivetextbox:
Exit Function
End Function
Public Property Set Parent(RHS As Object)
Set m_objParent = RHS
End Property
Private Sub m_UseMenu()
Dim lngIndex As Long
For lngIndex = 1 To m_cbrContextMenu.Controls.Count
Select Case m_cbrContextMenu.Controls(lngIndex).Tag
Case mCUT_TAG
Set m_cbtCut = m_cbrContextMenu.Controls(lngIndex)
Case mCOPY_TAG
Set m_cbtCopy = m_cbrContextMenu.Controls(lngIndex)
Case mPASTE_TAG
Set m_cbtPaste = m_cbrContextMenu.Controls(lngIndex)
End Select
Next
End Sub
Public Property Set TBox(RHS As MSForms.TextBox)
Set m_txtTBox = RHS
End Property
Private Sub Class_Initialize()
Set m_objDataObject = New DataObject
Set m_cbrContextMenu = m_GetEditContextMenu
If Not m_cbrContextMenu Is Nothing Then
m_UseMenu
End If
End Sub
Private Sub Class_Terminate()
Set m_objDataObject = Nothing
m_DestroyEditContextMenu
End Sub
Private Sub m_cbtCopy_Click(ByVal Ctrl As Office.CommandBarButton, CancelDefault As Boolean)
' check active textbox is this instance of CTextBox_ContextMenu
If m_ActiveTextbox() Then
With m_objDataObject
.Clear
.SetText m_txtTBox.SelText
.PutInClipboard
End With
End If
End Sub
Private Sub m_cbtCut_Click(ByVal Ctrl As Office.CommandBarButton, CancelDefault As Boolean)
If m_txtTBox.Locked = True Then
Exit Sub
End If
' check active textbox is this instance of CTextBox_ContextMenu
If m_ActiveTextbox() Then
With m_objDataObject
.Clear
.SetText m_txtTBox.SelText
.PutInClipboard
m_txtTBox.SelText = vbNullString
End With
End If
End Sub
Private Sub m_cbtPaste_Click(ByVal Ctrl As Office.CommandBarButton, CancelDefault As Boolean)
If m_txtTBox.Locked = True Then
Exit Sub
End If
' check active textbox is this instance of CTextBox_ContextMenu
On Error GoTo ErrPaste
If m_ActiveTextbox() Then
With m_objDataObject
.GetFromClipboard
m_txtTBox.SelText = .GetText
End With
End If
ErrPaste:
Exit Sub
End Sub
Private Sub m_txtTBox_MouseUp(ByVal Button As Integer, ByVal Shift As Integer, ByVal x As Single, ByVal y As Single)
If Button = 2 Then
' right click
m_cbrContextMenu.ShowPopup
End If
End Sub
What can I add to this code for the same context menu to apply with Images?
Something along the lines of...
Adding Private WithEvents m_imgImage As MSForms.Image
Private m_cbrContextMenu As CommandBar
Private WithEvents m_txtTBox As MSForms.TextBox
Private WithEvents m_imgImage As MSForms.Image
Private WithEvents m_cbtCut As CommandBarButton
Private WithEvents m_cbtCopy As CommandBarButton
Private WithEvents m_cbtPaste As CommandBarButton
Private m_objDataObject As DataObject
Private m_objParent As Object
Private Function m_CreateEditContextMenu() As CommandBar
Declaring a New Private Function
Private Function m_ActiveImage() As Boolean
'
' Make sure this instance is connected to active control
' May need to drill down through container controls to
' reach ActiveControl object
'
Dim objCtl As Object
Set objCtl = m_objParent.ActiveControl
Do While UCase(TypeName(objCtl)) <> "IMAGE"
If UCase(TypeName(objCtl)) = "MULTIPAGE" Then
Set objCtl = objCtl.Pages(objCtl.Value).ActiveControl
Else
Set objCtl = objCtl.ActiveControl
End If
Loop
m_ActiveImage = (StrComp(objCtl.Name, m_imgImage.Name, vbTextCompare) = 0)
ErrActiveimage:
Exit Function
End Function
I would need to declare a new Public Property Set
Public Property Set Img(RHS As MSForms.Image)
Set m_imgImage = RHS
End Property
Each context menu option would need altering to include the possibility of a user right clicking on an image...
Private Sub m_cbtCopy_Click(ByVal Ctrl As Office.CommandBarButton, CancelDefault As Boolean)
' check active image is this instance of CTextBox_ContextMenu
If m_ActiveTextbox() Then
With m_objDataObject
.Clear
.SetText m_txtTBox.SelText
.PutInClipboard
End With
End If
' check active image is this instance of CImage_ContextMenu
If m_ActiveImage() Then
With m_objDataObject
.Clear
'What would be the image alternative for this next line of code?
'.SetText m_imgImage.SelText
.PutInClipboard
End With
End If
End Sub
*You will note that I am only using the Copy feature of the context menu as Cutting and Pasteing from within an User form will not be required (or stable for that matter!).
And finally I would need to recreate the trigger...
Private Sub m_imgImage_MouseUp(ByVal Button As Integer, ByVal Shift As Integer, ByVal x As Single, ByVal y As Single)
If Button = 2 Then
' right click
m_cbrContextMenu.ShowPopup
End If
End Sub
It seems like an awful lot of unnecessary work, there must be an easier way.
Any help or advice is much appreciated, and once again thank you for your time.
Mr J.
If I have understood your question right, you just want to respond to all image click in one sub. This is how I do it. First create a class called ImageClickResponder (for this example) and add the following:
Option Explicit
Private Type Properties
Obj As Object
Procedure As String
CallType As VbCallType
End Type
Private this As Properties
Private WithEvents img As MSForms.Image
Public Sub Initialize(ByRef imgRef As MSForms.Image, ByRef Obj As Object, ByVal procedureName As String, ByVal CallType As VbCallType)
Set img = imgRef
With this
Set .Obj = Obj
.Procedure = procedureName
.CallType = CallType
Debug.Print imgRef.Name
End With
End Sub
Private Sub img_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
VBA.CallByName this.Obj, this.Procedure, this.CallType, Button, Shift, X, Y
End Sub
Then in your user form put this:
Option Explicit
Private micrs() As ImageClickResponder
Private Sub UserForm_Initialize()
micrs = LoadImageClickResponders(Me)
End Sub
Public Sub AllImgs_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Debug.Print "Your context menu code here"
End Sub
Private Function LoadImageClickResponders(ByRef frm As MSForms.UserForm) As ImageClickResponder()
Dim rtnVal() As ImageClickResponder
Dim ctrl As MSForms.Control
Dim i As Long
For Each ctrl In frm.Controls
If TypeOf ctrl Is MSForms.Image Then
ReDim Preserve rtnVal(i) As ImageClickResponder
Set rtnVal(i) = New ImageClickResponder
rtnVal(i).Initialize ctrl, Me, "AllImgs_MouseDown", VbMethod
i = i + 1
End If
Next
LoadImageClickResponders = rtnVal
End Function
I'm working in Excel on a Userform. Essentially, I want a "Photoshop-esque" toolbar that floats over my spreadsheet while I work, allowing me to select and use various tools.
I've got a series of toggle buttons set up so that when one is clicked, any other toggle buttons go back to unclicked. It's a little more complicated because I have sub-buttons, if that makes sense, meaning that once I have a button clicked, I can click one of four other buttons to make my actual selection, and these four buttons are mutually exclusive from each other as well.
The weird thing: I haven't been able to get these buttons to work. Except. For some reason, when I right-click only, the buttons work like a charm. Left-click: nothing. Help please?
Sample button code:
Private Sub tMouse_MouseUp(ByVal button As Integer, _
ByVal shift As Integer, ByVal X As Single, ByVal Y As Single)
tMouse.Value = True
tActual.Value = False
tSched.Value = False
tX.Value = False
tDiam.Value = False
tCirc.Value = False
tTri.Value = False
tTrash.Value = False
tText.Value = False
End Sub
EDIT:
I tried what was suggested about printing the value of the toggle button. And my computer blew up with messageboxes. I had changed all the actions to Click() events. Apparently I was sending the computer through an infinite loop. Maybe the act of changing a button from true to false or vice versa acts like a click and triggers all the other click events?
You have events that are triggering other events. What you'll need to do is set a Boolean AllowEventsToRun variable (either at the module or public level) and set that to false at the start of your code. Run whatever you need to do, and then set it to true at the end.
The trick is to do an if statement to make sure that AllowEventsToRun is set to true before any other code is running. Be sure to initialize the Boolean to true when you load your userform (since the default value of a boolean is false. So something like this:
Option Explicit
Private AllowEventsToRun As Boolean
Private Sub ToggleButton1_MouseUp(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
If AllowEventsToRun Then
AllowEventsToRun = False
'whatever you're doing that's causing the events to chain fire
AllowEventsToRun = True
End If
End Sub
Private Sub ToggleButton2_MouseUp(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
If AllowEventsToRun Then
AllowEventsToRun = False
'whatever you're doing that's causing the events to chain fire
AllowEventsToRun = True
End If
End Sub
Private Sub UserForm_Initialize()
AllowEventsToRun = True
End Sub
source: http://www.cpearson.com/excel/SuppressChangeInForms.htm
I would recommend using an "Option" Control instead of a "Toggle" Control.
If you stick 4 options in 1 frame then only 1 of those 4 options will allow itself to be true at a time automatically. You should use the "ToggleButton" control only if you want multiple instances of it to be true at the same time.
However if you refuse to do so and just really want to use ToggleButtons. Then you could write a procedure that is executed once pressing the button that sends it's name (or something else that identifies it uniquely) as a parameter to a procedure that sets all other togglebuttons false except it.
Cheers!
Private Sub ToggleButton1_Click()
Dim s As String
s = "ToggleButton1"
Evaluate_Options s
End Sub
Private Sub ToggleButton2_Click()
Dim s As String
s = "ToggleButton2"
Evaluate_Options s
End Sub
Private Sub ToggleButton3_Click()
Dim s As String
s = "ToggleButton3"
Evaluate_Options s
End Sub
Private Sub ToggleButton4_Click()
Dim s As String
s = "ToggleButton4"
Evaluate_Options s
End Sub
Private Sub ToggleButton5_Click()
Dim s As String
s = "ToggleButton5"
Evaluate_Options s
End Sub
Private Sub ToggleButton6_Click()
Dim s As String
s = "ToggleButton6"
Evaluate_Options s
End Sub
Private Sub ToggleButton7_Click()
Dim s As String
s = "ToggleButton7"
Evaluate_Options s
End Sub
Private Sub ToggleButton8_Click()
Dim s As String
s = "ToggleButton8"
Evaluate_Options s
End Sub
Private Sub Evaluate_Options(s As String)
Dim tgl As Control
For Each tgl In UserForm1.Frame1.Controls
If InStr(tgl.Name, s) Then Set_Toggles_1 tgl
Next
For Each tgl In UserForm1.Frame2.Controls
If InStr(tgl.Name, s) Then Set_Toggles_2 tgl
Next
End Sub
Private Sub Set_Toggles_1(tglTrue As Control)
Dim tglFalse As Control
For Each tglFalse In UserForm1.Frame1.Controls
If tglFalse.Name = tglTrue.Name Then tglFalse = True Else tglFalse = False
Next
End Sub
Private Sub Set_Toggles_2(tglTrue As Control)
Dim tglFalse As Control
For Each tglFalse In UserForm1.Frame2.Controls
If tglFalse.Name = tglTrue.Name Then tglFalse = True Else tglFalse = False
Next
End Sub
Try these basic mouse event capture subs and modify to suit your needs (tMouse = toggle button name):
Private Sub tMouse_Click()
'MsgBox "tb value = " & tMouse.Value
End Sub
Note: upper sub will not work, if lower sub called
Private Sub tMouse_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
If Button = 1 Then MsgBox "Left"
If Button = 2 Then MsgBox "Right"
End Sub