2024年11月27日水曜日

Excel VBA 備忘録 テクニック編

・画面表示を更新せずプログラムを実行する方法
    Rem 画面更新停止
    Application.ScreenUpdating = False
    Rem 画面更新再開
    Application.ScreenUpdating = True

・Me = 実行中のモジュールを示す変数
    MSのドキュメント「Me キーワード」 より抜粋

Meキーワード (keyword) は、暗黙的に宣言された変数のように動作します。 クラス モジュール内のすべてのプロシージャで自動的に使用できます。

クラスに複数のインスタンスが含まれる可能性がある場合、Me を使用して、コードが実行されているクラスの特定のインスタンスを参照できます。 現在実行されているクラスのインスタンスに関する情報を別のモジュール内のプロシージャに渡す場合、Me を使用すると特に便利です。

    例:実行中のWorksheetの名前の取得
        MsgBox Me.Name

 

2024年6月2日日曜日

Windows 11 Excel VBA で InternetExplorerが使えなくなったことへの対応

Excel VBAでこれまで使えていた InternetExplorerがWindows 11で使えなくなりました。(と思ったら、また使えるようになっていた。よくわからないが、いずれは使えなくなる前提でコーディングしておいた方がよいでしょう。)

Excelはサポート切れの2010と、ちと古いです。

ちょっと調べると、代替え策としてフォームのWebBrowserを使う方法が出てきます。Navigateでurlのページを表示するだけならこれが良さそうです。ですが、DOMのメソッドを使って操作する場合、使えないメソッドがいくつかあります。たとえば

でエラーが発生します。他にも使えないメソッドがあるかもしれません。

これへの解決策として、次のコードのようにDocument.Allを使って全エレメントを取得し、個々にnameやclassNameをチェックする方法が出てきます。

    Dim elm As HtmlElement     For Each elm In WebBrowser1.Document.All I       if elm.GetAttribute("className") = "someName" Then             Debug.Printh elm.InnerText         End If     Next

これは単発で使うには十分かと思いますが、ループで使用するにはいささか重い処理になりそうで、検討の余地ありです。加えてclassには複数の値が設定されることが多く、"="での比較はほとんど場合に不適切でしょう。

なお、「このページのスクリプトでエラーが発生しました」が発生する場合は、navigateを呼ぶ前に次の行を追加します。

    WebBrowser1.Silent = True     

次に見つけたのはMSXML2.XMLHTTPを使う方法です。次のようなコードでXmlDocumentを作ります。

    Dim MyRequest As Object     Set MyRequest = CreateObject("MSXML2.XMLHTTP")     MyRequest.Open "GET", url     MyRequest.send     Do Until MyRequest.readyState = 4: DoEvents: Loop     Set xmldoc = MyRequest.responseXML

これで成功する場合は良さそうですが、これは TLS 1.2 以上には対応していないという問題があるようです。次のようにするとよいという指摘があります。

    Set MyRequest = CreateObject("MSXML2.ServerXMLHTTP")

私は MSXML2.XMLHTTP.6.0 を使いましたが、今のところ問題ありません。

    Set MyRequest = CreateObject("MSXML2.XMLHTTP.6.0")

なお、MSXML2.XMLHTTP.6.0 でエラーが出る場合は参照を追加します。

    ツール⇒参照設定⇒Microsoft XML v6.0追加

    その他、次のライブラリ参照も必要です。
    Microsoft HTML Object Library
 後述のサンプルにあるRegExpを使うには、このライブラリも必要です。
    Microsoft VBScript Regular Expressions 5.5

さらに調べた結果、次の方法が見つかりました。これはHTMLをDOMDocumentとして取得し、それからHTMLDocumentを作ります。

これはよい結果でしたので、次のようなFunctionを作り、実際に使用しています。

Public Function GetHTMLDoc(url As String)     Dim httpReq As Object     Set httpReq = CreateObject("MSXML2.XMLHTTP.6.0")     httpReq.Open "GET", url, False     httpReq.send (Null)     Do Until httpReq.readyState = 4: DoEvents: Loop     Dim htmlDoc As IHTMLDocument     Set htmlDoc = New HTMLDocument     htmlDoc.Write httpReq.responseText     Set GetHTMLDoc = htmlDoc End Function

この方法はWebページを表示する必要がなく、DOMメソッドで操作するのが目的な場合に適しています。表示を行わないため、処理は高速です。表示も必要な場合は、WebBrowerにHTMLをセットすればよいでしょう(試していませんが)。

さて、これで作った HTMLDocument ですが、ちょっと奇妙な動きをします。

htmlDoc.GetElementsByClassName(className)

は動作します。(先頭の"G”は大文字です。)

ですが、個々の HtlmElement には getElementsByClassName を適用できません。

この範囲で十分であれば、従来のIEと同様の方法で利用することができます。

ですが、ある Element の子要素から getElementsByClassName で要素抽出をしたい場合には処理が複雑になります。

そこで、次のようなFunctionを作りました。あるエレメント内の指定のtagNameのものからclassNameを含むものを抽出します。

Public Function GetElementsByClassName(htmlElm As IHTMLElement, tagName As String, className As String)     Dim elm As Object, attr As String, list As Collection     Dim re As New RegExp: re.pattern = "(^| )" & className & "( |$)"     Set list = New Collection     For Each elm In htmlElm.getElementsByTagName(tagName)         If re.test(elm.getAttribute("className")) Then             list.Add elm         End If     Next     Set GetElementsByClassName = list End Function

正規表現はclass属性の最初、または最後か、前後がスペースで区切られているものとマッチします。

    re.pattern = "(^| )" & className & "( |$)" 

戻り値の型はCollectionになります。インデックスを使う場合、配列と異なり先頭の要素のインデックスは 1 になることに注意してください。

加えて、次のようなFunctionも作りました。私の場合、getElementsByClassNameで見つかった最初の要素を使うことが多く、IEを使った処理では次のようなコードでした。

    name = someElm.getElementsByClassName("name")(0).innerText

そこで、Collectionで返さず最初に見つかった要素を返すようにしました。

Public Function GetFirstElementByClassName(htmlElm As IHTMLElement, tagName As String, className As String)     Dim elm As Object, val As String     Dim re As New RegExp: re.pattern = "(^| )" & className & "( |$)"     For Each elm In htmlElm.getElementsByTagName(tagName)         If re.test(elm.getAttribute("className")) Then             Set GetFirstElementByClassName = elm             Exit Function         End If     Next     Set GetFirstElementByClassName = Nothing End Function  

呼び出し方は次のようになります。

    name = GetFirstElementByClassName(someElm, "div",  "name").innerText

戻り値がNothingとなる場合があるなら、次のようなチェックを入れます。

    Set elm = GetFirstElementByClassName(someElm, "div",  "name")     If Not elm Is Nothing Then name = elm.innerText

HtmlDocumentの先頭からclassNameのものを探したい場合は htmlDoc.body または htmlDoc.DocumentElement を htmlElm として渡します。

    divs = GetElementsByClassName(htmlDoc.body, "div",  "someClass")

私の場合はclassNameでエレメントを抽出する場合はtagNameも決まっているのでこの方が都合がよく、かつ処理速度も多少なりとも速くなっているだろうと思います。

もし異なるtagNameのものも含めて抽出したい場合は、たとえばtagNameにvbNullStringを渡し、次のような使い分けをすればよいでしょう。

    Dim elms as IHTMLElementCollection
    If tagName = vbNullString Then
        Set elms = htmlElm.all
    Else
        Set elms = htmlElm.getElementsByTagName(tagName)
    End If

結果的にはWebBrowserによる表示が不要なため、この変更で処理速度は格段に速くなりました。

補足

この例で生成する HtmlDocument にはいくつか制約があります。

・sectionなど非対応のtagがあり、その場合はHTMLUnknownElementとなり、単独のElementとして作られるがDOM構造には含まれず、getElementsByTagName、allなどのメソッド/プロパティーが使用できない。

原因ははっきりしませんが、フリーズすることがありました。速度が速くなりすぎ、Excelの再描画追いつかないためかもしれません。そんなときは再描画の一時中止/再開を試してみてください。  

    Application.ScreenUpdating = False         再描画が発生する処理     Application.ScreenUpdating = True 

 

2024年5月26日日曜日

Mac インストールUSBドライブ作成エラー ...app does not appear to be a valid OS installer application.

Webページ https://support.apple.com/ja-jp/101578 を参照し、
Macbool Air 2011 A3169 用に High Sierra のインストールUSBドライブを作成しようとしたらエラーが発生。

Webページ「ターミナルを使って起動可能なインストーラを作成する」に記載されているHigh Sierra用のコマンドをそのままコピペ(MyVolumeは適宜変更)でターミナルで実行するとエラーが出る。

コマンド
sudo /Applications/Install\ macOS\ High\ Sierra.app/Contents/Resources/createinstallmedia --volume /Volumes/MyVolume

エラー
/Applicaiont/macOS\ High\ Sierra.app does not appear to be a valid OS installer application.

これは日本語環境でAppStoreからインストーラをダウンロードするとインストーラの名前が

Install macOS High Sierra.app

ではなく

Install macOS High Sierraインスール.app

となり、異なっているため。どうも"Install macOS High Sierra.app”という名前をチェックしているようなので、ファイル名を英語表記に合わせて変更しないとダメなようだ。

MyVolumeは実際のターゲットのマウントポイント名に変更するが、実際のマウントポイントが表示名と異なることがある。この場合は見た目は一致していてもマウントポイントが見つからないというエラーが出る。

ディスクユーティリティーでUSBドライブのプロパティーを見て、コマンドのMyVolumeをマウントポイントに一致させる。

あるいはディスクユーティリティーでUSBドライブを消去し、名前を”MyVolume”でフォーマットすればコマンドはそのままでよい。

MyVolumeは createinstallmedia 実行後に既定の名前に変更される。










2024年4月7日日曜日

C# FormアプリでMicrosoftのライブラリだけでheic画像表示

WPFではサポートされてもFormアプリでは使えない機能があります。

Imageのheic対応もそのひとつで、次のようなコードでheicファイルを表示しようとするとメモリ不足のエラーが発生します。

pictureBox1.Image = Image.FromFile(mediaItem.FilePath);

Webでheic表示関連を検索するとMagick.NETを使う例が見つかりますが、Microsoftのライブラリだけで行う方法はまだ見つかりませんでした。Magick.NETは様々なフォーマットを扱える優れたものだと思いますが、dllをアプリに含めると、小さいなアプリでは本体よりMagick.NETの方が大きくなってしまいます。heicサポートの追加だけが目的な場合はあまり好ましくないので、Microsoftのライブラリだけで行う方法をいくつか試してみました。

なお、前提としてWindows 10, 11にHEIF画像拡張をインストールしてあるものとします。HEIF画像拡張がインストールされていない場合はMicrosoft Store から ダウンロードできます。

方法1
BitmapImage
BitmapEncoderを使用する。
今のところ、これが一番良好な結果になっています。
次のようなステップで行います。

System.Windows.Media.Imaging.BitmapImageをファイルから作る。BmpBitmapEncoderでMemoryStreamに書き出す。
MemoryStreamからSystem.Drawing.Imageを作る。
pictureBox1.Imageにセットする。

コードは次のようになります。

            BitmapImage bImage = new BitmapImage();
            bImage.BeginInit();
            bImage.CacheOption = BitmapCacheOption.None;
            bImage.DecodePixelHeight = height;
            bImage.DecodePixelWidth = width;
            bImage.UriSource = new Uri(FilePath, UriKind.Absolute);
            bImage.EndInit();
            using (MemoryStream ms = new MemoryStream())
            {
                BitmapEncoder enc = new BmpBitmapEncoder();
                enc.Frames.Add(BitmapFrame.Create(bImage));
                enc.Save(ms);
                pictureBox1.Image = Image.FromStream(ms);
            }

ひとつの静止画を扱うなら、これで十分でしょう。イメージのサイズを設定できるので、メモリ消費も必要最低限に抑えられ、スピードもこれ以上早くするのは難しいでしょう。

また、書き出し先をファイルにすればフォーマット変換にも使えます。

System.Windows.Media.Imagingのための参照追加 は次のとおりです。

プロジェクトの参照右クリック⇒参照の追加⇒アッセンブリ
PresentationCoreをチェック⇒OK

方法2
BitmapDecoder
BitmapEncoderを使用する。

BitmapDecoderの使い方はよくわかっていませんが、複数のFrameを持ち、Animationなども扱えます。複雑な画像表示を行えるようですが、ひとつの静止画だけを扱うなら方法1で十分そうです。もしかしたら有用かもしれないので紹介しておきます。

ステップは方法1のBitmapImageがSystem.Windows.Media.Imaging.BitmapDecoderに置き換わっただけです。

コードは次のようになります。

            BitmapDecoder uriBitmap = BitmapDecoder.Create(
                new Uri(FilePath, UriKind.Absolute),
      BitmapCreateOptions.None,
                BitmapCacheOption.Default);
            using (MemoryStream ms = new MemoryStream())
           {
               BitmapEncoder enc = new BmpBitmapEncoder();
               enc.Frames.Add(uriBitmap.Frames[0]);
               enc.Save(ms);
               pictureBox1.Image = Image.FromStream(ms);
           }

調べた限りでは、この流れでは画像サイズを指定できません。そのため、高解像度画像ではメモリ消費が多くなり、スピードも少し遅くなります。

方法3
 AxWMPLib.AxWindowsMediaPlayerを使用する。

MediaPlayerは静止画も表示でき、HEIF 画像拡張インストールされていればheicフォーマットも表示できます。ちなみにHEVCビデオも再生できます。静止画を扱う場合は uiMode="none"が良いでしょう。

メリットとしては、まずは静止画も動画も同じインタフェイスで扱えることです。また、対応していないフォーマットや壊れたファイルのエラー対応もMediaPlayerまかせにできます。

これまでのFormアプリと同じ要領で使え、次の要領でコントロールにファイルのパスを設定するだけなので、お手軽といえます。

(AxWMPLib.AxWindowsMediaPlayer)player.URL = filePath;

VisualStudioでMediaPlayerを使う方法は次のリンクを参照してください。
Microsoft Visual Studio で Windows メディア プレーヤー コントロールを使用する
https://draft.blogger.com/blog/post/edit/6538117324271148932/209217070553378409#

デメリットとしては、方法1,2に比べると遅いこと、メモリ消費量も特に高解像度のheicの場合は多くなります。

HEIFコーディックの有無チェック 
Magick.NETと違い、HEIFコーディックがないとエラーになります。
以下の要領でHEIFコーディックの有無がチェックできます。 

bool HeifDllExists = Registry.GetValue(@"HKEY_CLASSES_ROOT\CLSID\{E9A4A80A-44FE-4DE4-8971-7150B10A5199}\InprocServer32", null, null) != null;

参照:HEIF Format Overview
https://learn.microsoft.com/ja-jp/previous-versions/windows/desktop/legacy/mt846532(v=vs.85)




2024年2月25日日曜日

Excel VBA 備忘録 基礎知識編

ときどきExcelのVBAを書きますが、普段使わない言語だとなにかとつまづきます。
自分自身のための備忘録ですが、きっと初心者ならだれもつまづくところだと思います。

VariantとObject
    Variant
     すべての変数。
     Integerなどの値のみを持つプリミティブなデータ型とオブジェクト型のどちらも代入可能。
       配列もプリミティブなデータ型なのでVariantで宣言した変数にしか代入できない。    
    Object
     データとプロパティー、メソッドがカプセル化されたもの。
       オブジェクト型のみ代入可能。
       これにプリミティブなデータを代入すると「オブジェクトが必要です」エラーとなる。

Null, Nothing, Empty, vbNullString の違い
 Null             Variant型変数内に有効な値が入っていない状態
 Nothing       Objectとして空の状態
 Empty         Variant型として空の状態
   vbNullString 値が0のString 

代入時にSetが必要な場合、不要な場合
   Setが必要な場合
        Objectに代入する場合で、値だけでなくObjectのプロパティーの設定が行われる。
 Setが不要な場合
     Integerなどのデータ型に値のみ代入する場合。

Sub呼び出しでCallが必要な場合
 引数を()でくくる場合。

コンパイルエラー:プロパティーの使い方が不正です
    Setを付けずにObjectへ代入しようとした場合。

コンパイルエラー:型が一致しません
 Objectで宣言した変数ににIntegerなどの値をSetしようとした場合。
  Dim o as Object: Set o = 1

コンパイルエラー:オブジェクトが必要です
   Variantで宣言した変数に値をSetしようした場合。
  Dim v as Variant: Set v = 1
 Stringで宣言した変数にSetで文字列を代入しようとした場合。
  Dim s as String: Set s = "str"

コンパイルエラー:ユーザ定義型は定義されていません
    Dimや引数の型宣言で規定のデータ型にもユーザ定義型にもない名前が使われている場合。
    だいたいは書き間違え

実行時エラーオブジェクトが必要です
    値がNothingのオブジェクトのプロパティーにアクセスした場合。
    Objectで宣言した変数にプリミティブなデータを代入しようとした場合。
    Objectで宣言した変数にプリミティブなデータを代入しようとした場合。

実行時エラー:オブジェクト変数またはWithブロック変数が設定されていません。
    Objectで宣言した変数にSetをつけずにオブジェクト、または値を代入しようとした場合。
     Dim o as Object: o = "a" 
     Dim o as Object: o = 1 

実行時エラー 400