I have an excel vba application that runs a separate process. During the external process a progress indicator with abort button is initialized. It is crucial for the end users that the abort button is available. It is also usefull to see the external process running.
I would like to have the progress indicator/abort button placed on top of the external process. but I do NOT wont to force the userform on top of everything.
I have tried to use findwindow / setwindowpos, resulting in the following problems:
If I initialize HWND_TOPMOST before running the process, then the userform is always on top, regardless of what the user wants. I find this very annoying, especially if some sort of errors occur where the debuging window might be blocked by the inactive vba userform. However the workbook remains in the background which is desired.
If I use HWND_TOP (after the external process is up and running) then the entire workbook is activated (not just the userform), which then hides to progress of the external application. Not very benificial compared to activating the workbook.
Are there any suggestions on how to put the userform in front of the external applicaiton, while still allowing the user to deactivate it?
code snippets:
Option Explicit
' Code stolen with pride from various sources.
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Private Declare Function BringWindowToTop Lib "user32" (ByVal hwnd As Long) As Long
Private Declare Function SetForegroundWindow Lib "user32" (ByVal hwnd As Long) As Long
Private Declare Function SetWindowPos Lib "user32" ( _
ByVal hwnd As Long, _
ByVal hWndInsertAfter As Long, _
ByVal X As Long, _
ByVal Y As Long, _
ByVal cx As Long, _
ByVal cy As Long, _
ByVal wFlags As Long) As Long
Public Const SWP_NOMOVE = &H2
Public Const SWP_NOSIZE = &H1
Public Const HWND_TOP = 0
Public Const HWND_BOTTOM = 1
Public Const HWND_TOPMOST = -1
Public Const HWND_NOTOPMOST = -2
Public id As Integer
Public ProgressForm As fProgress
' Main routine, launch application and progress indicator.
Sub LoadForm()
Set ProgressForm = New fProgress
ProgressForm.Show
' Force userform to front, makes it on top but does not allow reorder of windows.
ForceToFront ProgressForm
' Run external process, notepad used for example.
id = Shell("notepad", vbNormalFocus)
End Sub
' Routine to bring userform to front after the external program is up and running.
Sub TestBringToFront()
BringToFront ProgressForm
End Sub
Sub BringToFront(fm As fProgress)
Dim hwnd As Long, ret As Variant
hwnd = FindWindow("ThunderDFrame", fm.Caption)
ret = SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE Or SWP_NOSIZE)
End Sub
Sub ForceToFront(fm As fProgress)
Dim hwnd As Long, ret As Variant
hwnd = FindWindow("ThunderDFrame", fm.Caption)
ret = SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE Or SWP_NOSIZE)
End Sub
Unfortunately this is the designated behaviour of windows and applications. However, I can suggest some workarounds:
Use TOPMOST option and make the userform clickable, e.g. capture the ProgressForm_Click and toggle between TOPMOST and NOTOPMOST state. ALternatively use can employ an "Always On Top" checkbox on ProgressForm like it is in the Task Manager.
Use TOP option and resize the workbook window behind to as small as possible, or even move aside (out of the screen), and restore size and position after the external app finished.
It may be worthy making a try with ProgressForm.Show(vbModeless) and TOP
Related
I'm running a PowerPoint Macro-Enabled Slide Show. When a user opens this file the presentation starts immediately. The presentation contains various shapes that, when pressed (use of links), will open a new Powerpoint Slide Show in front of the main Slide Show.
In the background I'm using VBA (code is located in the main Macro enabled slide show) to measure the time a user spends on all of the slides. I want the user to be able to stop this timer with a userform and a button. However, when a new Powerpoint Slide Show is opened, it appears in front of the main slide show. The userform will then disappears behind the new slide show. Using a second screen i have been able to view the userform. But when clicked on the userform it brings the main slide show in front of the other slide show.
So in short: I would like a userform that is in front of all slide show windows.
I tried using vbmodeless but this does not help. I've also tried out various bits of code:
http://www.vbaexpress.com/forum/showthread.php?58189-Make-userform-stay-on-top-of-all-windows-when-macro-is-fired
https://www.mrexcel.com/board/threads/userform-always-on-top.386643/
https://www.mrexcel.com/board/threads/keeping-userform-on-top-when-changing-active-workbook.1165439/
Unfortunately, none of these seem to be working. Some of these are for excel and I've not been able to rewrite these bits of code.
P.S. If this isn't possible, maybe I could hide the main slide show?
Simple version
Create a class module, MyClass, and put this code there:
Public WithEvents App As Application
Private Sub App_SlideShowBegin(ByVal Wn As SlideShowWindow)
UserForm1.Show False
End Sub
Create a module and put this code in it:
Dim MyThing As New MyClass
Sub InitializeApp()
Set MyThing.App = Application
End Sub
Run the InitializeApp method first. Now, when you start your presentation, your UserForm1 will show up. The False flag makes it non-modal, which is what I think you are looking for.
Slightly more advanced version
As above, but change the module to this:
Option Explicit
Public Const SWP_NOMOVE = &H2
Public Const SWP_NOSIZE = &H1
Public Const HWND_TOP = 0
Public Const HWND_BOTTOM = 1
Public Const HWND_TOPMOST = -1
Public Const HWND_NOTOPMOST = -2
Public Declare Function SetWindowPos Lib "user32" _
(ByVal hWnd As Long, _
ByVal hWndInsertAfter As Long, _
ByVal X As Long, _
ByVal Y As Long, _
ByVal cx As Long, _
ByVal cy As Long, _
ByVal uFlags As Long) As Long
Public Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
(ByVal lpClassName As String, _
ByVal lpWindowName As String) As Long
Dim MyThing As New MyClass
Sub InitializeApp()
Set MyThing.App = Application
End Sub
And add this to your form code:
Option Explicit
Option Explicit
Private Sub UserForm_Initialize()
Dim formHWnd As Long
formHWnd = FindWindow("ThunderDFrame", Me.Caption)
SetWindowPos formHWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE Or SWP_NOSIZE
End Sub
I have created a VB6 program which runs in the background to another program. It means the program window will be in the back only to this other program. I am using this code for it,
Private Declare Function FindWindow1 Lib "User32" Alias "FindWindowA" (ByVal lpclassname As String, ByVal lpWindowName As String) As Long
Private Declare Function SetWindowLong Lib "User32" Alias "SetWindowLongA" (ByVal hWnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Private Const GWL_HWNDPARENT = -8
Private parenthwnd As Long
Private strTitle As String
Private Sub Form_Load()
strTitle = "My Program" 'Title of the program window
parenthwnd = FindWindow1(vbNullString, strTitle)
Dim R As Long
R = SetWindowLong(parenthwnd, GWL_HWNDPARENT, Me.hWnd)
End Sub
The other program will run this VB6 program which sets it-self to the background to the other program window. It works. But there are two problems.
When the VB6 program executes the code R = SetWindowLong(parenthwnd, GWL_HWNDPARENT, Me.hWnd), the other program along with VB6 program goes to the background. How to make other program active when it is run and the VB6 program is executed?
When the other program is closed, it has the code to terminate the VB6 program. But this does not close the VB6 program. I think this may be due to running the code R = SetWindowLong(parenthwnd, GWL_HWNDPARENT, Me.hWnd). How to fix this?
If I understand you question correctly (and I'm not sure I do) here's the code I use to send a form to the background or send the form to the top. Perhaps it's what you're looking for.
' Declares and constants for BringToFront and SendToBack
Public Declare Function SetWindowPos Lib "user32" (ByVal _
hWnd As Long, ByVal hWndInsertAfter As Long, ByVal X As _
Long, ByVal Y As Long, ByVal cx As Long, ByVal cy As _
Long, ByVal wFlags As Long) As Long
Public Const SWP_NOMOVE = &H2
Public Const SWP_NOSIZE = &H1
Public Const HWND_BOTTOM = 1
Public Const HWND_TOP = 0
Public Sub BringToFront(frm As Form)
Dim flags As Long
flags = SWP_NOSIZE Or SWP_NOMOVE
SetWindowPos frm.hWnd, HWND_TOP, 0, 0, 0, 0, flags
End Sub
Public Sub SendToBack(frm As Form)
Dim flags As Long
flags = SWP_NOSIZE Or SWP_NOMOVE
SetWindowPos frm.hWnd, HWND_BOTTOM, 0, 0, 0, 0, flags
End Sub
I am trying to make a macro that records what a user clicked, which then records the mouse coordinates and the delay between the clicks. This will then repeat after some other SendKey changes. How can I detect when I click the mouse when the macro is running? I already know how to get the coordinates and record the delay, but what is the best course of action for detecting the mouse click and also what would be the best way to save all this information? A text file? Here is a snippet of the mouse click events that I use:
Public Declare Function SetCursorPos Lib "user32" (ByVal x As Long, ByVal y As Long) As Long
Public Declare Function GetCursorPos Lib "user32" (lpPoint As POINTAPI) As Long
Public Type POINTAPI
x As Long
y As Long
End Type
Public pos As POINTAPI ' Declare variable
Public Declare Sub mouse_event Lib "user32" (ByVal dwFlags As Long, ByVal dx As Long, ByVal dy As Long, ByVal cButtons As Long, ByVal dwExtraInfo As Long)
Public Const MOUSEEVENTF_LEFTDOWN = &H2
Public Const MOUSEEVENTF_LEFTUP = &H4
Public Const MOUSEEVENTF_RIGHTDOWN As Long = &H8
Public Const MOUSEEVENTF_RIGHTUP As Long = &H10
Public Sub SingleClick()
Dim xval, yval
xval = GetSetting("Will's Program Sheet", "DPS Calibration", "PROGRAM X")
yval = GetSetting("Will's Program Sheet", "DPS Calibration", "PROGRAM Y")
Select Case xval
Case Is = "" 'Runs calibrate if it can't find an xval
Call CALIBRATE
End
End Select
SetCursorPos xval, yval 'x and y position
mouse_event MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0
mouse_event MOUSEEVENTF_LEFTUP, 0, 0, 0, 0
End Sub
There is another macro that calls SingleClick where it moves to a constant x and y, clicks, does some magic, and returns to the position before the macro started. So to reiterate, is there a simple or easy to understand method to record multiple clicks and delays between clicks and replay them through Excel VBA?
This is theoretically possible to do, but you'd have to set a hook for WH_MOUSE_LL messages. The problem is that I seriously doubt that VBA can keep up with the volume of messages that are going to be coming through that pipe. It would be like trying drinking from a fire hose in VBA. If you really want to give it a shot, you can see if this works.
But first:
DISCLAIMER
In all likelihood, Excel will stop responding if you set up this Workbook and open it. It will certainlly stop responding if you open the VBE. Do not put this in a spreadsheet that you can't afford to delete. Be fully prepared to have to open it with the shift key down to make edits to the code. You have been warned. I take no responsibility for what you do with this. I know better than to have tried it with any code in the event handler. You will likely crash Excel. You will certainly crash the VBE. You may crash anything or everything else.
That should cover it. So...
In a class called HookHolder:
Option Explicit
Private hook As Long
Public Sub SetHook()
hook = SetWindowsHookEx(WH_MOUSE_LL, AddressOf ClickHook, _
0, GetCurrentThreadId)
End Sub
Public Sub UnsetHook()
'IMPORTANT: You need to release the hook when you're done with it.
UnhookWindowsHookEx hook
End Sub
In ThisWorkbook:
Option Explicit
Private danger As HookHolder
Private Sub Workbook_Open()
Set danger = New HookHolder
danger.SetHook
End Sub
Private Sub Workbook_BeforeClose(Cancel As Boolean)
danger.UnsetHook
End Sub
In a Module:
Option Explicit
Public Declare Function GetCurrentThreadId Lib "kernel32" () As Long
Public Declare Function SetWindowsHookEx Lib "user32" Alias "SetWindowsHookExA" _
(ByVal idHook As Long, ByVal lpfn As Long, ByVal hmod As Long, _
ByVal dwThreadId As Long) As Long
Public Declare Function UnhookWindowsHookEx Lib "user32" (ByVal hHook As Long) As Long
Public Declare Function CallNextHookEx Lib "user32" (ByVal hHook As Long, _
ByVal nCode As Long, ByVal wParam As Long, lParam As Any) As Long
Public Const HC_ACTION As Long = 0
Public Const WH_MOUSE_LL As Long = &H2
Public Const WM_LBUTTONDOWN As Long = &H201
Public Const WM_LBUTTONUP As Long = &H202
Public Const WM_LBUTTONDBLCLK As Long = &H203
'Your callback function.
Public Function ClickHook(ByVal nCode As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
If nCode = HC_ACTION Then
'Anything in particular you're interest in?
Select Case wParam
Case WM_LBUTTONDOWN
'Do your thing.
Case WM_LBUTTONUP
'Do your thing.
Case WM_LBUTTONDBLCLK
'Do your thing.
End Select
End If
CallNextHookEx 0, nCode, wParam, ByVal lParam
End Function
Icons disappearing in PopUp menu after setting the main (first) PopUp menu item Visible property to False, but they come back when Visible property switched back to Enable, why?
Option Explicit
Private Const MF_BITMAP = &H4&
Private Const MF_BYPOSITION As Long = &H400& '&H404&
Private Declare Function GetMenu Lib "user32" (ByVal hwnd As Long) As Long
Private Declare Function GetSubMenu Lib "user32" (ByVal hMenu As Long, ByVal nPos As Long) As Long
Private Declare Function SetMenuItemBitmaps Lib "user32" (ByVal hMenu As Long, ByVal nPosition As Long, ByVal wFlags As Long, ByVal hBitmapUnchecked As Long, ByVal hBitmapChecked As Long) As Long
Public Sub SetPopUpMenuIcons(objFRM As Form)
On Error Resume Next
Dim lgRet As Long
Dim lgMnu As Long
Dim lgSubMnu As Long
lgMnu = GetMenu(objFRM.hwnd)
lgSubMnu = GetSubMenu(lgMnu, 0)
With objFRM
lgRet = SetMenuItemBitmaps(lgSubMnu, 0, MF_BYPOSITION, .imgMnu(0).Picture, 0)
lgRet = SetMenuItemBitmaps(lgSubMnu, 1, MF_BYPOSITION, .imgMnu(1).Picture, 0)
lgRet = SetMenuItemBitmaps(lgSubMnu, 2, MF_BYPOSITION, .imgMnu(2).Picture, 0)
lgRet = SetMenuItemBitmaps(lgSubMnu, 3, MF_BYPOSITION, .imgMnu(3).Picture, 0)
lgRet = SetMenuItemBitmaps(lgSubMnu, 4, MF_BYPOSITION, .imgMnu(4).Picture, 0)
End With
Exit Sub
End Sub
I believe that the VB6 PopupMenu method actually creates a new popup menu (by calling CreatePopupMenu) and then uses the hidden menu items as templates for creating items via InsertMenu or AppendMenu, but without using the bitmap flag and handles which it doesn't know you added behind its back.
You might have to do all of the work yourself, calling CreatePopupMenu and then InsertMenuItem instead of InsertMenu/AppendMenu for each item (in order to have both a bitmap and text), and use subclassing to get the selection "event" if any, and so on.
In other words there may be no trivial way to do what you want. The VB6 menu plumbing seems to be based on old 16-bit Windows (Win 3.x) logic and API calls.
Too bad we never got a real VB7! Microsoft had accumulated a vast list of such feature enhancements before pulling the plug in favor of .Net.
There may be a 3rd party menu control that could make this easier.
I want to develop an app which won't allow the user to open or jump to another application while it is open. It should be in Visual Basic. For example, if my application is open (running) and the user tries to open any other windows application like "media player" then it shouldn't open. The app should not even allow "task manager" to run. The application should completely block the windows environment while it is running.
A very good question. :)
Is is possible to achieve it in VB?
The answer is Yes!
Is it Easy?
Definitely not!
However here are few tips on how to approach the problem.
1) Disable the Task Manager
Sub DisableTaskManager()
Shell "REG add HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\System /v DisableTaskMgr /t REG_DWORD /d 1 /f", vbNormalFocus
End Sub
Sub EnableTaskManager()
Shell "REG add HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\System /v DisableTaskMgr /t REG_DWORD /d 0 /f", vbNormalFocus
End Sub
2) Ensure your program is always on top
a) Hide the task bar
Option Explicit
'~~> http://allapi.mentalis.org/apilist/FindWindow.shtml
Private Declare Function FindWindow Lib "user32" Alias _
"FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName _
As String) As Long
'~~> http://allapi.mentalis.org/apilist/SetWindowPos.shtml
Private Declare Function SetWindowPos Lib "user32" _
(ByVal hwnd As Long, ByVal hWndInsertAfter As Long, _
ByVal x As Long, ByVal y As Long, ByVal cx As Long, _
ByVal cy As Long, ByVal wFlags As Long) As Long
Private Const SWP_HIDEWINDOW = &H80
Private Const SWP_SHOWWINDOW = &H40
'~~> Show/Hide Taskbar
Sub Sample()
'~~> To show the taskbar
ShowTskBar True
'~~> To hide the taskbar
ShowTskBar False
End Sub
Sub ShowTskBar(ShouldI As Boolean)
Dim Sid As Long
Sid = FindWindow("Shell_traywnd", "")
If ShouldI = True Then
If Sid > 0 Then _
Sid = SetWindowPos(Sid, 0, 0, 0, 0, 0, SWP_SHOWWINDOW)
Else
If Sid > 0 Then _
Sid = SetWindowPos(Sid, 0, 0, 0, 0, 0, SWP_HIDEWINDOW)
End If
End Sub
b) Show your application Always on top
'~~> http://www.allapi.net/apilist/SetWindowPos.shtml
Private Declare Function SetWindowPos Lib "user32" _
(ByVal hwnd As Long, ByVal hWndInsertAfter As Long, _
ByVal x As Long, ByVal y As Long, ByVal cx As Long, _
ByVal cy As Long, ByVal wFlags As Long) As Long
Const HWND_TOPMOST = -1
Const HWND_NOTOPMOST = -2
Const SWP_NOSIZE = &H1
Const SWP_NOMOVE = &H2
Const SWP_NOACTIVATE = &H10
Const SWP_SHOWWINDOW = &H40
Private Sub Form_Activate()
SetWindowPos Me.hwnd, HWND_TOPMOST, 0, 0, 0, 0, _
SWP_NOACTIVATE Or SWP_SHOWWINDOW Or SWP_NOMOVE Or SWP_NOSIZE
End Sub
b) Show your application in maximized mode
Maximize your form so that the desktop shows only your form as it shows in a Kiosk application. Depending on the need you can also disable the minimize button or the title bar. In such a case do remember to add a button so that user can click that to exit the form.
3) Disable the Start Menu
This code depends on the Windows version that you are using. Do a search on Google, you will find plenty of examples.
Similarly, you have to take care of few small small things but this post will give you a good start. If you are looking for a complete solution in one place then I doubt you will ever get it ;)
HTH
Take a look at the Desktop APIi to create your own "sandbox" but very careful as it's very easy to lock yourself out of the primary desktop.
Also see this question for a bit more information.