Will a kind soul help to provide a vba solution to press a button (e.g 3) in Microsoft calculator without using sendkeys?

API_newnoob

New Member
Joined
Jun 6, 2020
Messages
21
Office Version
  1. 2016
Platform
  1. Windows
Hi all,



Will a kind soul help to provide a vba solution to press a button (e.g 3) in Microsoft calculator without using sendkeys?
 
Dear @Jaafar Tribak ,
Will you be able to give me a code to show me how to activate button 7 on the calculator based on the Control ID from spy++?

Sorry, I was cut off the internet for almost a day.

This is how I did to write number 7 (This applies to the classic calc.exe (Standard Mode) prior to windows10)

VBA Code:
Option Explicit

#If VBA7 Then
    Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr
    Declare PtrSafe Function FindWindowEx Lib "user32" Alias "FindWindowExA" (ByVal hWnd1 As LongPtr, ByVal hwnd2 As LongPtr, ByVal lpsz1 As String, ByVal lpsz2 As String) As LongPtr
    Declare PtrSafe Function GetNextWindow Lib "user32" Alias "GetWindow" (ByVal hwnd As LongPtr, ByVal wFlag As Long) As LongPtr
    Declare PtrSafe Function GetDlgItem Lib "user32" (ByVal hDlg As LongPtr, ByVal nIDDlgItem As Long) As LongPtr
    Declare PtrSafe Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal hwnd As LongPtr, ByVal wMsg As Long, ByVal wParam As LongPtr, ByVal lParam As String) As LongPtr
#Else
    Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
    Declare Function FindWindowEx Lib "user32" Alias "FindWindowExA" (ByVal hWnd1 As Long, ByVal hwnd2 As Long, ByVal lpsz1 As String, ByVal lpsz2 As String) As Long
    Declare Function GetNextWindow Lib "user32" Alias "GetWindow" (ByVal hwnd As Long, ByVal wFlag As Long) As Long
    Declare Function GetDlgItem Lib "user32" (ByVal hDlg As Long, ByVal nIDDlgItem As Long) As Long
    Declare Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal lParam As String) As Long
#End If


Sub Example()

    Const BM_CLICK = &HF5
    Const GW_CHILD = &H5
    Const GW_HWNDNEXT = &H2

    #If VBA7 Then
        Dim hCurrentDlg As LongPtr
    #Else
        Dim hCurrentDlg As Long
    #End If

    hCurrentDlg = FindWindow("CalcFrame", vbNullString)
    hCurrentDlg = FindWindowEx(hCurrentDlg, 0, "CalcFrame", vbNullString)

    hCurrentDlg = GetNextWindow(hCurrentDlg, GW_CHILD)
    hCurrentDlg = GetNextWindow(hCurrentDlg, GW_HWNDNEXT)
    hCurrentDlg = GetNextWindow(hCurrentDlg, GW_HWNDNEXT)

    Call SendMessage(GetDlgItem(hCurrentDlg, &H89), BM_CLICK, 0, 0)

End Sub




You can wrap this into a generic routine to write numbers more flexibly :
VBA Code:
Sub WriteNum(ByVal Num As Long)

    Const BM_CLICK = &HF5
    Const GW_CHILD = &H5
    Const GW_HWNDNEXT = &H2

    #If VBA7 Then
        Dim hCurrentDlg As LongPtr
    #Else
        Dim hCurrentDlg As Long
    #End If

    Dim vCtrIDsArray() As Variant
    Dim lCtrID As Long, i As Long

    'Store ctrl IDs (Corresponding to buttons from 0 to 9)
    vCtrIDsArray = Array(&H82, &H83, &H84, &H85, &H86, &H87, &H88, &H89, &H8A, &H8B)

    'find the target dialog window.
    hCurrentDlg = FindWindow("CalcFrame", vbNullString)
    hCurrentDlg = FindWindowEx(hCurrentDlg, 0, "CalcFrame", vbNullString)
    hCurrentDlg = GetNextWindow(hCurrentDlg, GW_CHILD)
    hCurrentDlg = GetNextWindow(hCurrentDlg, GW_HWNDNEXT)
    hCurrentDlg = GetNextWindow(hCurrentDlg, GW_HWNDNEXT)

    'Clean the screen.
    Call SendMessage(GetDlgItem(hCurrentDlg, &H51), BM_CLICK, 0, 0)

    'Send each digit at a time.
    For i = 1 To Len(CStr(Num))
        lCtrID = Mid(Num, i, 1) + &H82
        If Not IsError(Application.Match(lCtrID, vCtrIDsArray, 0)) Then
            Call SendMessage(GetDlgItem(hCurrentDlg, lCtrID), BM_CLICK, 0, 0)
        End If
    Next

End Sub

And then use it like this:
VBA Code:
Sub Test()
    Call WriteNum(Num:=1234)
End Sub


One thing about Calc.exe is that its control buttons have their respective hwnds and CtrlIDs but don't have a window text despite displaying text such 1,2, 3 etc .
I mention this because it would have been much easier and the code much shorter if we could refer to a ctrl by its text.

Another subject you may want to study later on is programming using the Microsoft Active Accessibility (MSAA)
MSSAA and UIAutomation approaches can be a very useful alternatives (sometimes it is the only way) to access\execute a ctrl in some Dialogs\Windows.. A case in point is when dealing with window-less controls.
 
Upvote 0

Excel Facts

Workdays for a market open Mon, Wed, Friday?
Yes! Use "0101011" for the weekend argument in NETWORKDAYS.INTL or WORKDAY.INTL. The 7 digits start on Monday. 1 means it is a weekend.
Dear @Jaafar Tribak ,

I would like to thank you firstly for taking the time to come up with a macro to let me study and understand further.

I definitely saw a few things that i don't understand immediately, for e.g (why is it &89 and not 0000000000000089? What does the & means and when do i apply it.)

As my knowledge is really bad, please give me some time to google around and try my best to figure out all functions and how it is being used.

Once i have tried my best, i will then ask the remaining doubts i still have. I really appreciate your kind effort taken to educate a programming idiot like me.
 
Upvote 0
I definitely saw a few things that i don't understand immediately, for e.g (why is it &89 and not 0000000000000089? What does the & means and when do i apply it.)

The 0000000000000089 you see in spy is not 89. It is actually the HexaDecimal representation of 137 (Decimal).

The vba Hex function returns the Hexadecimal representaion of a decimal number
Debug.Print Hex(137) '<== gives you 89

and &H89 gives you the Decimal representation:
Debug.Print &H89 '<== gives you 137

&H0000000000000089 is the same as &H89 ( just like in decimals, 000123 is the same as 123) ... 0s on the left side are neutral and the vb editor compiler just removes them.

The reason the 0s are shown in spy is because they are represented as a string maybe to show the maximum byte size of the number. In this case : 8 Bytes=64bits .

In VBA, you don't often encounter the need to deal with Hex, Oct, Bin.. etc. But if you call API functions from Windows dlls in your vba program then it is recommended that you understand at least the basics of HEX,BIN numerical systems as windows dlls, I believe, were written in C and the MS Documentation for many API functions make use of those numerical systems for constants , memory addresses and so on.

I suggest that you google around for understanding binary hexadecimal programming . You will find plenty of detailed info on the subject.

Good luck with your search.
 
Upvote 0
Dear @Jaafar Tribak ,

Thank you for the solution, the solution has further deepened my knowledge.

I do have a couple of questions which i hope you can explain.

1) Len(CStr(Num)) <-- Is the CStr function really necessary ? The code seems to run without an error even without the convert to string function. Can you explain the rationale of why you used the function ?

2) Below is how i would code to change the view to a calculator view.

AppActivate "Calculator"
SendKeys "%vs", True

As you can see, this seems to be the "cheating" method.
Is there a method to call the ribbon/toolbar in an orthodox manner to tell it to change it to the scientific view?
The reason why i am asking is because i am afraid that some programs do not have shortcuts.
 
Upvote 0
Dear @Jaafar Tribak ,

Please ignore the previous reply.

Thank you for the solution, the solution has further deepened my knowledge.

I do have a couple of questions which i hope you can explain.

1) Len(CStr(Num)) <-- Is the CStr function really necessary ? The code seems to run without an error even without the convert to string function.
Can you explain the rationale of why you used the function ?

2) Below is how i would code to change the view to a calculator view.

AppActivate "Calculator"
SendKeys "%vs", True

As you can see, this seems to be the "cheating" method.
Is there a method to call the ribbon/toolbar in an orthodox manner to tell it to change it to the scientific view?

3) In your opinion, is it better to use hex for large numbers because it would take less digits than in decimal?
Using BM_CLICK as an example:

Const BM_CLICK = &HF5
Const BM_CLICK = 245

Does VBA converts hex to dec or dec to hex when running?
If VBA converts hex to dec and if i want to speed things up, should i declare 245 then?
 
Upvote 0
1) Len(CStr(Num)) <-- Is the CStr function really necessary ? The code seems to run without an error even without the convert to string function.
Can you explain the rationale of why you used the function ?

2) Below is how i would code to change the view to a calculator view.

3) In your opinion, is it better to use hex for large numbers because it would take less digits than in decimal?

1) The vba Len function returns the number of characters of the string that is passed to it. However, if the function is not passed a string, it behaves differently and instead of returning the number of characters it returns the number of Bytes of the variable passed to it.

Try passing 123 instead of 1234 and it will error out at the MID function call because in this case, the Len function returns 6 (bytes) instead of 3 (Characters)

The reason 1234 didn't cause an error is pure coincidence: Number of characters =4 and number of bytes is 4 (Long Variable)

By coercing the Num variable into a String using CStr, the Len function returns the number of characters in the Num Variable rather than the number of bytes in the Num Varaible.

Take a look at the Len function documentation: Len function (Visual Basic for Applications)


2) AFAIK, a server application can be accessed and automated by client applications in 3 ways (Each to a different extent).

a- OLE Automation like when controling WinWord from Excel via WinWord Object Model. (Gives almost full control)
b- API calls like what we have done here (Gives much less control than OLE automation and relies on window handles which is not always possible as many controls are window-less controls and\or don't have a hwnd, ID or Text)
c- MSAA and IUIAutomation (Again, much less control than OLE Automation plus not all server applications provide these two interfaces)

In the case of Calc.exe, OLE automation is not possible.
What you can do with API calls is limited (Calc.exe menus have no HWND or IDs so you can't activate them with API calls )
As for MSAA and IUIAutomation, Calce.exe Menu items are accessible but for some obscure reason their AccDoDefault Method (to actually activate them) is not available.

So, in short, you can't reliably activate Calc.exe menus with any of the above mentioned methods and you are left with the unreliable SendKeys option or with sending shaky API mouse clicks.

3) &H is just an hexadecimal represention of a number. Internally, whether you write &H1 or 1 it is the same for the compiler.

The reason you see a lot of hexadecimal in API programming, is because APIs are written in the C langauge which, unlike VB, supports memory pointers and can natively access and manage memory locations.

Hexadecimal representations are just a convinient way for the programmer to visualize memory in terms of Byte sequences..

When programming with Windows APIs, sooner or later, you will face a situation where you need to read some memory locations.
For example say an API function returns an integer value in of one of its arguments. Let's assume this integer value is in a variable called lParam .

lParam is an integer which is 2 Bytes or 16 bits of sequenced memory . Now suppose the first and second bytes each contain a byte value that we need in our code.

If we use hexadecimal to represent the value in lParam, the programmer can more easily visualize its contenets byte by byte.

Suppose lParam contains the integer value : 8069
The Hex representation of the Decimal 8069 is 1F 85. (High Byte = 1F, Low Byte = 85)
As you can see, it is now clearer to see what's going on in each Byte of the memory occupied by lParam and we now know the Byte values that we need in our code.

To extract, in code, the low order and High order Bytes of the lParam Integer, we can use 3 different ways : BitMasking, RtlMoveMemory or LSet

Here is an example that shows how to extract (using the 3 methods) the lower and higher Bytes of the integer value : 8069

VBA Code:
Option Explicit

Type MyInteger
    intValue As Integer
End Type

Type MyIntegerBytes
    LowOrder As Byte
    HighOrder As Byte
End Type

#If VBA7 Then
    Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)
#Else
    Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
#End If

Sub example()

    Dim intVar As Integer
 
    intVar = 8069 '<== &H1F85
    Debug.Print Hex(intVar) & " Hex" & " =" & intVar & " Dec"
    Debug.Print "*****************************************"
    Debug.Print "Extract Low and High Bytes of " & Hex(intVar)
 
    Debug.Print "*****************************************"
        Debug.Print "Using  BitMask "
        Debug.Print Space(10) & Hex(intVar And &HFF) & vbTab & "LoByte"  ' extract low order Byte
        Debug.Print Space(10) & Hex(((intVar And &HFF00) \ &H100) And &HFF) & vbTab & "HiByte"  ' extract high order Byte
     
    Debug.Print "*****************************************"
        Debug.Print "Using  RtlMoveMemory API "
        Dim LowOrder As Byte, HighOrder As Byte
        CopyMemory LowOrder, intVar, 1 ' extract low order Byte
        CopyMemory HighOrder, ByVal VarPtr(intVar) + 1, 1 ' extract high order Byte
        Debug.Print Space(10) & Hex(LowOrder) & vbTab & "LoByte"
        Debug.Print Space(10) & Hex(HighOrder) & vbTab & "HiByte"
     
    Debug.Print "*****************************************"
        Debug.Print "Using  LSet "
        Dim tMyInteger As MyInteger, tMyIntegerBytes As MyIntegerBytes
        tMyInteger.intValue = intVar
        LSet tMyIntegerBytes = tMyInteger
        Debug.Print Space(10) & Hex(tMyIntegerBytes.LowOrder) & vbTab & "LoByte"  ' extract low order Byte
        Debug.Print Space(10) & Hex(tMyIntegerBytes.HighOrder) & vbTab & "HiByte" ' extract high order Byte

End Sub

This all looks rather daunting but with practice it becomes a bit clearer. I myself still don't fully understand many aspects of API programming despite the fact that I have been making use of a lot of APIs in my vba codes but I keep studying and trying.

If you are intersted in learning about this API programming stuff , I suggest that you start by learning the basics of binary and Hex arithmetic. There is plenty of material on the subject in the internet.

Good luck.
 
Upvote 0

Forum statistics

Threads
1,223,227
Messages
6,170,849
Members
452,361
Latest member
d3ad3y3

We've detected that you are using an adblocker.

We have a great community of people providing Excel help here, but the hosting costs are enormous. You can help keep this site running by allowing ads on MrExcel.com.
Allow Ads at MrExcel

Which adblocker are you using?

Disable AdBlock

Follow these easy steps to disable AdBlock

1)Click on the icon in the browser’s toolbar.
2)Click on the icon in the browser’s toolbar.
2)Click on the "Pause on this site" option.
Go back

Disable AdBlock Plus

Follow these easy steps to disable AdBlock Plus

1)Click on the icon in the browser’s toolbar.
2)Click on the toggle to disable it for "mrexcel.com".
Go back

Disable uBlock Origin

Follow these easy steps to disable uBlock Origin

1)Click on the icon in the browser’s toolbar.
2)Click on the "Power" button.
3)Click on the "Refresh" button.
Go back

Disable uBlock

Follow these easy steps to disable uBlock

1)Click on the icon in the browser’s toolbar.
2)Click on the "Power" button.
3)Click on the "Refresh" button.
Go back
Back
Top