UI Automation用 SendKeysラッパー関数 (PowerShell)

概要

個人的に作っている UI Automation 関数群を、記憶を元に再構築、リファインした物の一部。

指定した要素にSendKeysをするだけのもの。 Pattern が使えれば不要なことは多いが、たまに必要になることも……。

この記事における UI Automation

.NET Framework の System.Windows.Automation 名前空間で定義されているもののこと。

動作確認環境

WIndows 10 Pro 64bit Windows PowerShell 5.1

コード

github.com

# 使用するアセンブリや名前空間の指定(PowerShell 5.1以降の機能)
using namespace System

# UIAutomation 関連のアセンブリ群
using assembly  UIAutomationClient
using assembly  UIAutomationTypes
using assembly  UIAutomationClientSideProviders
using namespace System.Windows.Automation

# SendKeys 用のアセンブリ
using assembly  System.Windows.Forms
using namespace System.Windows.Forms

function Send-UIAKeys {
<#
.SYNOPSIS
対象の要素にキーストロークを送信します。
.DESCRIPTION
$InputObjectで指定された要素にキーストロークを送信します。
System.Windows.Forms.SendKeys.SendWaitを使用するため、アクティブなウィンドウが変更されます。
#>
    [CmdletBinding()]
    [OutputType([System.Windows.Automation.AutomationElement])]
    Param(
        
        # キーストロークを送信する要素を指定します。
        # キーボードフォーカスを受け取ることが出来ればフォーカスし、そうでなければ直近の親ウィンドウを最前面にします。
        # このパラメーターは必須です。
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [AutomationElement]$InputObject
        ,
        # 送信するキーストロークを指定します。
        # System.Windows.Forms.SendKeys クラスと同じ形式で文字列を指定します。
        # https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.forms.sendkeys?view=netframework-4.8
        # このパラメーターは必須です。
        [Parameter(Mandatory = $true)]
        [string]$Keys
        ,
        # キーストローク送信後待機する時間をミリ秒単位で指定します。
        # $RestoreFocus スイッチを指定する場合は意図した動作になるよう調整が必要です。
        [ValidateRange(0, [int]::MaxValue)]
        [Alias('ms')]
        [int]$WaitMilliseconds = 0
        ,
        # キーストローク送信後、フォーカスを直前の要素に戻します。
        # 既定では、キーストロークを送信した要素が最前面となります。
        # 指定する場合、$WaitMilliseconds の値も適切な値に変更する必要があります。
        # この関数を連続して実行する場合、期待した結果が得られないことがあります。
        [switch]$RestoreFocus
        ,
        # $InputObject を再度パイプラインに出力します。
        # 既定では、この関数による出力はありません。
        [switch]$PassThru
    )

    Process {
        # 現在のフォーカスを取得。
        [AutomationElement]$currentFocus = [AutomationElement]::FocusedElement
        

        # 親ウィンドウを取得するため、WindowPatternを実装している要素を探すTreeWalkerを作成。
        [TreeWalker]$windowWalker = [TreeWalker]::new(
            [PropertyCondition]::new(
                [AutomationElement]::IsWindowPatternAvailableProperty, 
                $true
            )
        )

        # 親ウィンドウ取得。
        [AutomationElement]$parentWin = $windowWalker.Normalize($InputObject)
        if ($null -eq $parentWin) {
            # 取得できなかった場合は強制停止。
            $PSCmdlet.ThrowTerminatingError([Management.Automation.ErrorRecord]::new(
                [InvalidOperationException]::new('親ウィンドウを取得できません。'),
                'ParentWindowNotFound',
                [Management.Automation.ErrorCategory]::NotEnabled,
                $InputObject
            ))
        }

        # フォーカスの変更。
        # WindowPattern.SetWindowVisualState(最大化・最小化などの変更)を行うと、
        # 現在の状態にかかわらずそのウィンドウが最前面になることを利用。
        [WindowPattern]$winPtn = $parentWin.GetCurrentPattern([WindowPattern]::Pattern)
        [WindowVisualState]$visState = $winPtn.Current.WindowVisualState
        if ($visState -eq [WindowVisualState]::Minimized) {
            # 最小化されている場合は通常に戻す。
            $visState = [WindowVisualState]::Normal
        }
        $winPtn.SetWindowVisualState($visState)

        if ($parentWin.Current.IsKeyboardFocusable) {
            $parentWin.SetFocus()
        }
        if ($InputObject.Current.IsKeyboardFocusable) {
            $InputObject.SetFocus()
        }
        
        # キーストローク送信。
        [SendKeys]::SendWait($Keys)

        # 送信後の待機。
        while (-not $winPtn.WaitForInputIdle($WaitMilliseconds)) {
        }
        Start-Sleep -Milliseconds $WaitMilliseconds


        if ($RestoreFocus) {
            # フォーカスを戻す。
            $currentFocus.SetFocus()
        }
    }
}

動作イメージ

Start-Processで起動したメモ帳に九九の表を入力するコード。 Start-Processの代わりにGet-Processなどで Excel を取得しても動作する。

f:id:imihito:20191118235344g:plain
Send-UIAKeys動作イメージ

using namespace System
using namespace System.Diagnostics

using assembly  UIAutomationClient
using assembly  UIAutomationTypes
using assembly  UIAutomationClientSideProviders
using namespace System.Windows.Automation

[Process]$targetProc = Start-Process -FilePath notepad -PassThru
$targetProc.WaitForInputIdle()

[AutomationElement]$uiaTarget = [AutomationElement]::FromHandle($targetProc.MainWindowHandle)

for ($r = 1; $r -le 9; ++$r) {
    for ($c = 1; $c -le 9; ++$c) {
        # 引数指定で実行。
        Send-UIAKeys -InputObject $uiaTarget -Keys "$($r * $c){TAB}"
    }
    # パイプライン入力で実行。
    $uiaTarget | Send-UIAKeys -Keys "{ENTER}"
}

UI AutomationでExcelのセルを操作してみたかった(未完)

メモ程度。

このツイートの内容の確認に使用したコード。

<#
.Synopsis
# UI Automation でExcelのセルの値を取得するサンプル
## 前提条件
- Excelを起動し、何かブックを開いていること
- Windows 10 の Windows PowerShell ISE で実行すること
#>

# 実行に必要なアセンブリ類のロード
using assembly  UIAutomationClient
using assembly  UIAutomationTypes
using assembly  UIAutomationClientSideProviders
using namespace System.Windows.Automation

# Excelのウィンドウを取得。
# 今のデスクトップのルートの子どもから「XLMAIN」というクラス名の要素を探索。
[AutomationElement]$uiaXl =
    [AutomationElement]::RootElement.FindFirst(
        [TreeScope]::Children,
        [PropertyCondition]::new([AutomationElement]::ClassNameProperty, 'XLMAIN')
    )

# 取得したExcelのウィンドウ配下からテーブルとしての機能を持つ要素を探索。
[AutomationElement]$uiaCellTable = 
    $uiaXl.FindFirst(
        [TreeScope]::Descendants, 
        [PropertyCondition]::new([AutomationElement]::IsTablePatternAvailableProperty, $true)
    )

# テーブルとしての機能を使えるようにする。
[TablePattern]$ptnTable = $uiaCellTable.GetCurrentPattern([TablePattern]::Pattern)

# 今見えている範囲の左上から3,3の位置の要素を取得する(行・列見出しも含めて数える/左上がA1でない場合はB2ではない)。
[AutomationElement]$uiaB2 = $ptnTable.GetItem(2, 2)
<# 持っている機能の確認
PS > $uiaB2.GetSupportedPatterns()
   Id ProgrammaticName                       
   -- ----------------                       
10002 ValuePatternIdentifiers.Pattern        
10007 GridItemPatternIdentifiers.Pattern     
10010 SelectionItemPatternIdentifiers.Pattern
10013 TableItemPatternIdentifiers.Pattern    
10014 TextPatternIdentifiers.Pattern
#>

# 選択する機能
[SelectionItemPattern]$ptnSel = $uiaB2.GetCurrentPattern([SelectionItemPattern]::Pattern)
$ptnSel.Select() # セルの選択は可能

# 値の取得・設定をする機能
[ValuePattern]$ptnVal = $uiaB2.GetCurrentPattern([ValuePattern]::Pattern)
$ptnVal.Current.Value # セルに表示されている値を出力

<# SetValueでエラーは出ないけど表示に反映されない
$ptnVal.SetValue('Hoge')
$ptnVal.Current.Value # ここの値では反映されている
#>

型を検索する PowerShell 関数(親クラス→子クラス)

はじめに

ネット環境無しで PowerShell を弄っているとたまに起こるのが、「引数に何を渡せば良いのかわからない」問題です。

Get-Member コマンドレットなどで各種メンバーの定義は確認できますが、引数の型が抽象的な型になっていて、具体的な型がわからない、という問題です。

例:System.DateTimeToString メソッド

PS >[datetime]::Now.ToString.OverloadDefinitions

string ToString()
string ToString(string format)
string ToString(System.IFormatProvider provider)
string ToString(string format, System.IFormatProvider provider)
string IFormattable.ToString(string format, System.IFormatProvider formatProvider)
string IConvertible.ToString(System.IFormatProvider provider)

System.IFormatProvider って具体的に何……?」という問題です。

型名の先頭にIが付いていることからインターフェイスということはわかりますが、これだけではその先に繋がりません。

Microsoft Docs を見られれば、派生が書いてあるのでそれでOKなのですが……。 IFormatProvider Interface (System) | Microsoft Docs

対策の方針

今使えるすべての型の中から、該当する型の子クラス(やや不正確な表現)を探索する。

System.AppDomain を使えばロード済みアセンブリを取得できるので、さらにそのアセンブリ内の型を列挙すれば、使えるすべての型を取得できる。

Type.IsAssignableFrom(Type) Method (System) | Microsoft Docs を使えば、該当する型の子クラスかどうかも判定できる。

作成した関数

github.com

<#
.SYNOPSIS
Search type from loaded assemblies.
ロード済みアセンブリー内から型を検索します。
.DESCRIPTION
Search type by root type from Loaded assemblies.
ロード済みアセンブリー内から、指定した型及びサブクラスを検索します。
.EXAMPLE
[System.IFormatProvider] | Search-Type
IsPublic IsSerial Name               BaseType     
-------- -------- ----               --------     
True     False    IFormatProvider                 
True     True     CultureInfo        System.Object
True     True     DateTimeFormatInfo System.Object
True     True     NumberFormatInfo   System.Object
.INPUTS
System.Type
.OUTPUTS
System.Type
By default, return all public type in loaded assemblies.
#>
function Search-Type {
    [CmdletBinding()]
    [OutputType([type])]
    param (
        [Parameter(ValueFromPipeline=$true)]
        [type]$RootType = [System.Object]
        ,
        [SupportsWildcards()]
        [string]$Name
        ,
        [SupportsWildcards()]
        [string]$Namespace
        ,
        [SupportsWildcards()]
        [string]$FullName
    )
    begin {
        # Declare foreach variables for IntelliSense.
        [System.Reflection.Assembly]$asm = [type]$t = $null
    }
    process {
        foreach ($asm in [System.AppDomain]::CurrentDomain.GetAssemblies()) {
            foreach ($t in $asm.GetTypes()) {
                # Public only.
                if (-not $t.IsPublic) { continue }

                if (-not $RootType.IsAssignableFrom($t)) { continue }

                # Name check.
                if (-not [string]::IsNullOrEmpty($Name)      -and ($t.Name      -notlike $Name)      ) { continue }
                if (-not [string]::IsNullOrEmpty($Namespace) -and ($t.Namespace -notlike $Namespace) ) { continue }
                if (-not [string]::IsNullOrEmpty($FullName)  -and ($t.FullName  -notlike $FullName)  ) { continue }

                Write-Output -InputObject $t
            }
        }
    }
}

使用例1

前述のSystem.IFormatProviderを探したい場合は以下のようにする。

PS> [System.IFormatProvider] | Search-Type

IsPublic IsSerial Name               BaseType     
-------- -------- ----               --------     
True     False    IFormatProvider                 
True     True     CultureInfo        System.Object
True     True     DateTimeFormatInfo System.Object
True     True     NumberFormatInfo   System.Object

さらに以下のようにすれば、FullNameも分かるのであとはある程度何とかなる。

PS> [System.IFormatProvider] | Search-Type | Select-Object -ExpandProperty FullName

System.IFormatProvider
System.Globalization.CultureInfo
System.Globalization.DateTimeFormatInfo
System.Globalization.NumberFormatInfo

使用例2

どんなコレクションがあるんだっけ…?と発作的に調べてくなったら以下のようにする(ワイルドカード指定のサンプル)。

PS> [System.Collections.IEnumerable] | Search-Type -Namespace System.Collections*

IsPublic IsSerial Name                             BaseType                                                     
-------- -------- ----                             --------                                                     
True     True     CollectionBase                   System.Object                                                
True     True     DictionaryBase                   System.Object                                                
True     True     ReadOnlyCollectionBase           System.Object                                                
True     True     Queue                            System.Object                                                
True     True     ArrayList                        System.Object                                                
True     True     BitArray                         System.Object                                                
True     True     Stack                            System.Object                                                
True     True     Hashtable                        System.Object                                                
True     False    ICollection                                                                                   
True     False    IDictionary                                                                                   
True     False    IEnumerable                                                                                   
True     False    IList                                         
...

めもがき

曲線の始点終点どっちが近いのかな?判断

曲線の始点終点どっちが近いのかな?判断 - C#ATIA

関連のなにか。

実環境が無いのでスペルミス上等ということで。

Option Explicit

Private Sub Sample(crv As INFITF.Reference, pln As INFITF.Reference)
    
    Dim measureCrv As SPATypeLib.Measurable
    Set measureCrv = GetMeasurable(crv)
    Select Case measureCrv.GeometryName
        Case CatMeasurableCurve, CatMeasurableCircle, CatMeasurableLine 'OK
        Case Else: Err.Raise 13
    End Select
    
    Dim A As Double, B As Double, C As Double, D As Double
    ComputePlaneEquationABCD pln, A, B, C, D
    
    Dim onCrvPointsCoordinates(0 To 8) As Variant
    Call asDisp(measureCrv).GetPointsOnCurve(onCrvPointsCoordinates)
    
    'Start point coordinates
    Dim ptX As Double, ptY As Double, ptZ As Double
    ptX = onCrvPointsCoordinates(0)
    ptY = onCrvPointsCoordinates(1)
    ptZ = onCrvPointsCoordinates(2)
    
    '面の方程式と、始点から伸びる直線上の点の座標の方程式を解く
    
    'Origin point coordinates as ptX, ptY, ptZ
    'Plane projection point coordinates as prjX, prjY , prjZ
    'Distance of origin to projection as L
    
    'A * prjX + B * prjY + C * prjZ = D
    'prjX = ptX + A * L
    'prjY = ptY + B * L
    'prjZ = ptZ + C * L
    
    Dim L As Double
    L = (D + A * ptX + B * ptY + C * ptZ) / _
        (A ^ 2 + B ^ 2 + C ^ 2)
    Dim prjX As Double, prjY As Double, prjZ As Double
    prjX = ptX + A * L
    prjY = ptY + B * L
    prjZ = ptZ + C * L
    
    Debug.Print ComputeScalar(ptX - prjX, ptY - prjY, ptZ - prjZ)
    
End Sub

'Plane Equation
'Ax + By + Cz = D
Private Sub ComputePlaneEquationABCD( _
              iPlane As INFITF.Reference, _
        ByRef oA As Double, _
        ByRef oB As Double, _
        ByRef oC As Double, _
        ByRef oD As Double _
    )
    
    Dim measurePln As SPATypeLib.Measurable
    Set measurePln = GetMeasurable(iPlane)
    Select Case measureCrv.GeometryName
        Case CatMeasurablePlane 'OK
        Case Else: Err.Raise 13
    End Select
    
    Dim planeComponents(0 To 8) As Variant
    Call asDisp(measurePln).GetPlane(planeComponents)
    
    
    Dim x1st As Double, y1st As Double, z1st As Double
    x1st = planeComponents(3)
    y1st = planeComponents(4)
    z1st = planeComponents(5)
    
    Dim x2nd As Double, y2nd As Double, z2nd As Double
    x2nd = planeComponents(6)
    y2nd = planeComponents(7)
    z2nd = planeComponents(8)
    
    Dim planeNomalDirection() As Double
    planeNomalDirection = CrossProduct( _
        x1st, y1st, z1st, _
        x2nd, y2nd, z2nd _
    )
    
    Let oA = planeNomalDirection(0)
    Let oB = planeNomalDirection(1)
    Let oC = planeNomalDirection(2)
    
    Dim plnOriginX As Double, plnOriginY As Double, plnOriginZ As Double
    plnOriginX = planeComponents(0)
    plnOriginY = planeComponents(1)
    plnOriginZ = planeComponents(2)
    
    Let oD = ComputeScalar(plnOriginX, plnOriginY, plnOriginZ)
End Sub

'ベクトルの外積
Public Function CrossProduct( _
        iX1 As Double, iY1 As Double, iZ1 As Double, _
        iX2 As Double, iY2 As Double, iZ2 As Double _
    ) As Double() 'Double(0 To 2)
    
    Const X = 0, Y = 1, Z = 2
    Dim resultVector(0 To 2) As Double
    resultVector(X) = iY1 * iZ2 - iZ1 * iY2
    resultVector(Y) = iZ1 * iX2 - iX1 * iZ2
    resultVector(Z) = iX1 * iY2 - iY1 * iX2
    
    Let CrossProduct = resultVector
End Function

'ベクトルから大きさを求める
Public Function ComputeScalar( _
                 iX As Double, _
                 iY As Double, _
        Optional iZ As Double = 0# _
    ) As Double
    
    Let ComputeScalar = VBA.Math.Sqr(iX ^ 2 + iY ^ 2 + iZ ^ 2)
    
End Function

'てきとう
Public Function GetMeasurable(iRef As INFITF.Reference) As SPATypeLib.Measurable
    Dim doc As INFITF.Document
    Set doc = GetModelElement(iRef).Document
    Dim spaWb As SPATypeLib.SPAWorkbench
    Set spaWb = doc.GetWorkbench("SPAWorkbench")
    Set GetMeasurable = spaWb.GetMeasurable(iRef)
End Function

'[選択要素からドキュメントを取得する - C#ATIA](http://kantoku.hatenablog.com/entry/2016/04/07/183709 "選択要素からドキュメントを取得する - C#ATIA")
Public Function GetModelElement(iAnyObject As INFITF.AnyObject) As INFITF.ModelElement
    Set GetModelElement = iAnyObject.GetItem("ModelElement")
End Function

'disable VBE static syntax check.
Private Function asDisp(o As INFITF.CATBaseDispatch) As INFITF.CATBaseDispatch
    Set asDisp = o
End Function

参考

選択要素からドキュメントを取得する - C#ATIA
GetDirectionが上手く行かない2 - C#ATIA

何度でもよみがえるメモ帳(ネタ)

とあるソフトを間違えて閉じてしまうことが頻発したため、終了してもゾンビのごとく蘇るようにしてみた。

もっと良い方法がありそう……。

# メモ帳を起動してイベントを購読する処理
[scriptblock]$startNotepad = {
    # メモ帳を起動
    [Diagnostics.Process]$notepadProc = 
        Start-Process -FilePath notepad -PassThru
    # 起動を待機
    $notepadProc.WaitForInputIdle() > $null
    # イベントを通知させる
    $notepadProc.EnableRaisingEvents = $true

    # Exited(終了時)のイベントを購読開始
    Register-ObjectEvent -InputObject $notepadProc -EventName Exited
}

# 無限ループ
while ($true) {
    # メモ帳を起動
    $startNotepad.Invoke()
    
    # 何かしらイベントが起きるまで待つ
    Wait-Event

    # 発生したイベント情報を取得して破棄(破棄しないと`Wait-Event`で待機しない)
    Get-Event | Remove-Event
}

PowerShellなり、Windows PowerShell ISEなりに貼り付けて実行すると、何回閉じても復活するメモ帳が起動する。

終了したい場合は、PowerShellのウィンドウでCtrl+Cを押すか、PowerShellそのものを終了する。


190111追記

そもそもイベントにする必要が無かった。

# 無限ループ
while ($true) {
    # メモ帳を起動
    [Diagnostics.Process]$notepadProc = Start-Process -FilePath notepad -PassThru
    # 終了を待機
    $notepadProc.WaitForExit()
}

【VBA実験】何回NotしてもTrueになるTrueを作る

あけましておめでとうございます。 今年もよろしくお願いいたします。

前書き

私が職場で使っているVBAのライブラリの中には、「何回NotしてもTrueになるTrue」を返すAPIを持つものがあります(一般には使われていないライブラリ)。

この「何回NotしてもTrueになるTrue」をVBAだけで作成する方法の記事となります。
そのため、実際のコードに役立つことはほぼ無いでしょう。

VBAのBoolean周りの動作

本来、VBAのTrueは、16bitの符号付き整数で表すと-1になります(VBAの16進数表現で&HFFFF)。
これを、Not演算子でビット単位で否定すると0&H0000)、すなわちFalseになります。

対して、問題のTrueは16bitの符号付き整数で表すと1&H0001)になっており、これに対してNot演算子を使用すると-2&HFFFE)、0ではないためTrueになります。

コード

以下のコードにあるGetTrueNotTrue()関数の返り値が「何回NotしてもTrueになるTrue」になります。

Private Type intType
    Value As Integer
End Type

Private Type boolType
    Value As Boolean
End Type

Function GetTrueNotTrue() As Boolean
    Dim i As intType
    i.Value = 1
    
    Dim b As boolType
    LSet b = i
    
    Let GetTrueNotTrue = b.Value
    
End Function

コードの解説

普通に数値をBoolean型の変数に代入しても、自動型変換が行われ、VBA本来のTrue・Falseとして代入されてしまいます。
今回は数値のバイナリ表現を保ったまま、Boolean型の変数に代入したいため、別の方法をとる必要がありました。

上記のコードでは、ユーザー定義型のバイナリコピーができるLSetステートメントを使用し、Integer型の1のバイナリ表現をBoolean型にコピーしています。

Private Type intType
    Value As Integer
End Type

Private Type boolType
    Value As Boolean
End Type

で入れ物となるユーザー定義型を定義します (VBAではIntegerもBooleanも2バイトの領域を必要とします)。

    Dim i As intType
    i.Value = 1

でInteger型の1を設定、

    Dim b As boolType
    LSet b = i

で別のユーザー定義型にバイナリコピーしています。

動作の確認

Private Sub Sample()
    Debug.Print GetTrueNotTrue()        '-> True
    Debug.Print Not GetTrueNotTrue()    '-> True
    Debug.Print CInt(GetTrueNotTrue())  '-> 1
End Sub

無事、「何回NotしてもTrueになるTrue」を作成できました。

タスクバーに通知を表示するPowerShellスクリプト

小ネタ

以下のようなメッセージを簡単に表示できるPowerShellスクリプト
表示はそれぞれWIn 8.1 Win10

f:id:imihito:20180822223221p:plain

f:id:imihito:20180822223007p:plain

PowerShellスクリプト

本体。適当な場所に「○○.ps1」として保存する。

param(
    [string]$Prompt = 'メッセージ',
    [string]$Title  = '通知',
    $CallBack = ''
)

Add-Type -AssemblyName System.Windows.Forms, System.Drawing

function Show-NotifyIcon {
    param(
        [string]$Prompt = 'メッセージ',
        [string]$Title  = '通知',
        [scriptblock]$CallBack = {}
    )
    
    [Windows.Forms.NotifyIcon]$notifyIcon =
        New-Object -TypeName Windows.Forms.NotifyIcon -Property @{
            BalloonTipIcon  = [Windows.Forms.ToolTipIcon]::Info
            BalloonTipText  = $Prompt
            BalloonTipTitle = $Title
            Icon    = [Drawing.SystemIcons]::Information
            Text    = $Title
            Visible = $true
        }
    
    # イベント定義
    $notifyIcon.add_BalloonTipClicked( $CallBack )
    
    [int]$timeout = 3 # sec

    [DateTimeOffset]$finishTime = 
        [DateTimeOffset]::UtcNow.AddSeconds( $timeout )

    $notifyIcon.ShowBalloonTip( $timeout )
    
    # そのままだとイベントが走らない&すぐに消えてしまうので適当wait
    while ( [DateTimeOffset]::UtcNow -lt $finishTime ) {
        Start-Sleep -Milliseconds 1
    }
    $notifyIcon.Dispose()
}

$parameters = $MyInvocation.BoundParameters
$parameters.CallBack = [scriptblock]::Create( $CallBack )
Show-NotifyIcon @parameters

使い方

引数1:表示するメッセージ
引数2:タイトル
引数3:クリックされたときのコールバック処理
を文字列で渡す。

バッチ

powershell.exe -Sta -NoProfile -WindowStyle Hidden -ExecutionPolicy RemoteSigned -File 上記のps1ファイル メッセージ タイトル コールバック処理

VBA

VBAならWindows API使えば?という話はさておく。

Sub ShowNotifySample()
    Const NotifyScriptPath = "保存したスクリプト(ps1ファイル)の保存場所"
    Const PsCommandLineBase = "powershell.exe -Sta -NoProfile -WindowStyle Hidden -ExecutionPolicy RemoteSigned -File """
    Const WQuoteSpaceWQuote = """ """
    
    '表示するメッセージ及びタイトル
    Dim notifyMessage As String
    notifyMessage = "めっせーじ"
    Dim notifyTitle As String
    notifyTitle = "たいとる"
    
    'クリックされた時の処理(PowerShellスクリプト)
    'エクスプローラーでエクセルの場所を開く
    Dim callBackPsScript As String
    callBackPsScript = "explorer.exe " & Excel.Application.Path
    
    Dim execCmd As String
    execCmd = PsCommandLineBase & NotifyScriptPath & WQuoteSpaceWQuote & _
              notifyMessage & WQuoteSpaceWQuote & _
              notifyTitle & WQuoteSpaceWQuote & _
              callBackPsScript & """"
    
    Call VBA.Shell(execCmd, vbHide)
    
End Sub