はじめに
やりたいこと
社内でサイネージのように常時稼働しているモニターがあり、特定の時間帯にのみExcelを起動して、一定時間経過後にExcelを停止したいという要望があった
できていたこと
前任者がタスクスケジューラに設定を行い、自動起動を行うとともに、Excel自体にApplication.Ontimeを用いて指定時間後にExcelを終了させるロジックが組んであった
課題
タスクスケジューラから起動した場合に、Excelが最前面に表示されず、他のアプリケーションの背面に表示される状況となってしまっていた。そのため、気づいた人がアプリケーションを最前面化するなどの操作を行う必要があり、面倒であった。
対策
ThisWorkBookから呼び出す関数は、Excelの画面表示がなされる前に処理されるので、ThisWorkBookから呼び出すのは踏み台の関数にして、その踏み台の関数から非同期で本来実行したかった処理を加えるようにした
方針
対処方法
すでに前任者が組んでいる仕組みで利用者が慣れている状況であるため、大幅な仕様変更は行わず、現行運用継続できる方法で進める。
試行錯誤
ThisWorkBookからの呼び出し
Excelブックが開かれる際に呼び出されるThisWorkBookから新規作成する関数(window_maximize)を呼び出し、その処理で最前面化を図ろうとした。
Declare PtrSafe Function FindWindow Lib "user32" _
Alias "FindWindowA" (ByVal lpClassName As String, _
ByVal lpWindowName As String) As LongPtr
' Excelウィンドウを最大化、最前面で表示させる
Sub window_maximize()
'アプリケーションウィンドウを最大化
Application.WindowState = xlMaximized
'ドキュメントウィンドウ最大化
ThisWorkbook.Windows(1).WindowState = xlMaximized
' アプリケーションを最前面化
hwnd = Application.hwnd ' 自身のウィンドウハンドルを取得
SetForegroundWindow hwnd ' ウィンドウハンドルを指定して最前面化
End Sub
うまくいかない
検証環境において、上記のスクリプトを以下の条件で流してみる。
- Excelを起動している状態で、VBAのエディタを開く
- Excelを他のアプリケーションの背面に配置
- VBAのエディタからwindows_maximiz実行
背面に配置してたExcelが最前面に最大化されて表示された。
また、検証環境において、当該Excelを起動させると最前面に最大化で表示されるため、これで問題ないのだろうと思い、本番環境に適用した。
翌朝、指定時間になってサイネージを確認すると、最前面に設定されていなかった。
レジストリ編集
以下のサイトを参考にして「HKCU\Control Panel\Desktop\ForegroundLockTimeout」の設定を0に変更してみるが、うまく効いているようには見えない。
そもそもタスクスケジューラからタスクを実行するとExcelがタスクスケジューラの画面より前に出てこないのだから、最前面化には失敗しているのが見て取れる。
https://www.chuken-engineer.com/entry/2019/12/25/122328#google_vignette
強制的に最前面に持ってくる
以下のサイトを参考にして、最前面のアプリケーションのウィンドウタイトルが自身のExcelではない場合に、Shift+Alt+Tabキーを送信することで、強制的に最前面に持ってくるように処理を書いた
https://vba-create.jp/vba-error-appactivate/#google_vignette
Option Explicit
#If Win64 Then
'【SetForegroundWindow関数】
Declare PtrSafe Sub SetForegroundWindow Lib "user32" (ByVal hwnd As LongPtr)
'【FindWindow関数】
Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr
Dim hwnd As LongPtr
'【GetForegroundWindow関数】
Declare PtrSafe Function GetForegroundWindow Lib "user32" () As Long
'【GetWindowText関数】
Declare PtrSafe Function GetWindowText Lib "user32" Alias "GetWindowTextA" (ByVal hwnd As Long, ByVal lpString As String, ByVal cch As Long) As Long
#Else
'【SetForegroundWindow関数】
Declare Sub SetForegroundWindow Lib "user32" (ByVal hwnd As Long)
'【FindWindow関数】
Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Dim hwnd As Long
'【GetForegroundWindow関数】
Declare Function GetForegroundWindow Lib "user32" () As Long
'【GetWindowText関数】
Declare Function GetWindowText Lib "user32" Alias "GetWindowTextA" (ByVal hwnd As Long, ByVal lpString As String, ByVal cch As Long) As Long
#End If
' Excelウィンドウを最大化、最前面で表示させる
Sub window_maximize()
' ウィンドウタイトルの格納変数
Dim fTitle As String
'アプリケーションウィンドウを最大化
Application.WindowState = xlMaximized
'ドキュメントウィンドウ最大化
ThisWorkbook.Windows(1).WindowState = xlMaximized
' アプリケーションを最前面化
hwnd = Application.hwnd ' 自身のウィンドウハンドルを取得
SetForegroundWindow hwnd ' ウィンドウハンドルを指定して最前面化
' フォアグランド(最前面)のタイトルバーテキストを取得
fTitle = String(100, Chr(0))
GetWindowText GetForegroundWindow, fTitle, Len(fTitle)
fTitle = Left$(fTitle, InStr(fTitle, Chr(0)) - 1)
' フォアグランド(最前面)のウィンドウが自信のExcelでない場合、「SHIFT+ALT+TAB」をSendKeyで送る
If fTitle <> Application.Caption Then
Application.SendKeys "+%{tab}", True
End If
End Sub
やっぱりうまくいかない
タスクスケジューラからExcelを起動し、ThisWorkBookからwindow_maximizeを呼び出して動かしていると、どうにもうまくいかない。
正確に言うと、タスクスケジューラから呼び出した「Excelの画面が表示される前」にApplication.SendKeysに設定したキーが飛んでいるということに気付いた。
つまりThisWorkBookから呼び出した関数はExcelの画面が表示されるよりも前に実行されているように見える。となれば、表示する前のウィンドウを最前面にすることなどできないのだから、この状況になるのは理解できる。
ならやることは一つ
実際に動かしたい関数を非同期で遅延実行をしてあげればよいのでしょうね
' 5秒後にwindows_maximizeを呼び出す
Sub callWindow_maximize()
Call Application.OnTime(Now + TimeValue("00:00:05"), "window_maximize")
End Sub
上記の関数を作成し、ThisWorkBookから呼び出す関数を「window_maximize」ではなく「callWindows_maximize」に変更してやった。
結論
得た知見
少なくとも私の環境においては、ThisWorkBookの処理はExcelの画面表示よりも前に処理をされるので、表示されてから実際に動かしたい処理がある場合、正しく動かなかった
ThisWorkBook⇒呼び出し用の関数⇒OnTimeで指定秒数を経過後に⇒実際に実行したい関数を呼び出すことで今回の課題はクリアできた。
なんでなのかはわからないが、こうなってしまうので仕方ない。
世の中でこのような状況についてのメモを見つけることができなかったので、メモとして残す。誰かの何かの助けになれば幸いである。