sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

C# Core Audio API を使ってみる

Core Audio APIWindows Vista から追加された OS のスピーカー設定(音量など)にアクセスするためのものです。

OS のスピーカーまわりをイジるには丁度よい API なのですが、C# から利用できるライブラリーがどうも用意されていなかった(C++ 利用のライブラリーだった)のですが、P/Invoke を使って利用してみたサンプルを公開。

音量を変更するための P/Invoke

音量変更するために必要なクラスやメソッドを定義します。これが面倒で C# から利用されないんだろうなぁ。COM DLL の CLSID を調べて明記して……ということをしないといけないので、かなりめんどくさい。(PreserveSig 属性だらけになってるのも)

namespace CoreAudioManager
{
    [ComImport]
    [Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
    internal class MMDeviceEnumerator
    {
    }

    internal enum EDataFlow
    {
        eRender,
        eCapture,
        eAll,
        EDataFlow_enum_count
    }

    internal enum ERole
    {
        eConsole,
        eMultimedia,
        eCommunications,
        ERole_enum_count
    }

    [Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IMMDeviceEnumerator
    {
        int NotImpl1();

        [PreserveSig]
        int GetDefaultAudioEndpoint(EDataFlow dataFlow, ERole role, out IMMDevice ppDevice);

        // the rest is not implemented
    }

    [Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IMMDevice
    {
        [PreserveSig]
        int Activate(ref Guid iid, int dwClsCtx, IntPtr pActivationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);

        // the rest is not implemented
    }

    [Guid("77AA99A0-1BD6-484F-8BC7-2C654C9A9B6F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IAudioSessionManager2
    {
        int NotImpl1();
        int NotImpl2();

        [PreserveSig]
        int GetSessionEnumerator(out IAudioSessionEnumerator SessionEnum);

        // the rest is not implemented
    }

    [Guid("E2F5BB11-0570-40CA-ACDD-3AA01277DEE8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IAudioSessionEnumerator
    {
        [PreserveSig]
        int GetCount(out int SessionCount);

        [PreserveSig]
        int GetSession(int SessionCount, out IAudioSessionControl2 Session);
    }

    [Guid("87CE5498-68D6-44E5-9215-6DA47EF883D8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface ISimpleAudioVolume
    {
        [PreserveSig]
        int SetMasterVolume(float fLevel, ref Guid EventContext);

        [PreserveSig]
        int GetMasterVolume(out float pfLevel);

        [PreserveSig]
        int SetMute(bool bMute, ref Guid EventContext);

        [PreserveSig]
        int GetMute(out bool pbMute);
    }

    [Guid("bfb7ff88-7239-4fc9-8fa2-07c950be9c6d"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IAudioSessionControl2
    {
        // IAudioSessionControl
        [PreserveSig]
        int NotImpl0();

        [PreserveSig]
        int GetDisplayName([MarshalAs(UnmanagedType.LPWStr)] out string pRetVal);

        [PreserveSig]
        int SetDisplayName([MarshalAs(UnmanagedType.LPWStr)] string Value, [MarshalAs(UnmanagedType.LPStruct)] Guid EventContext);

        [PreserveSig]
        int GetIconPath([MarshalAs(UnmanagedType.LPWStr)] out string pRetVal);

        [PreserveSig]
        int SetIconPath([MarshalAs(UnmanagedType.LPWStr)] string Value, [MarshalAs(UnmanagedType.LPStruct)] Guid EventContext);

        [PreserveSig]
        int GetGroupingParam(out Guid pRetVal);

        [PreserveSig]
        int SetGroupingParam([MarshalAs(UnmanagedType.LPStruct)] Guid Override, [MarshalAs(UnmanagedType.LPStruct)] Guid EventContext);

        [PreserveSig]
        int NotImpl1();

        [PreserveSig]
        int NotImpl2();

        // IAudioSessionControl2
        [PreserveSig]
        int GetSessionIdentifier([MarshalAs(UnmanagedType.LPWStr)] out string pRetVal);

        [PreserveSig]
        int GetSessionInstanceIdentifier([MarshalAs(UnmanagedType.LPWStr)] out string pRetVal);

        [PreserveSig]
        int GetProcessId(out int pRetVal);

        [PreserveSig]
        int IsSystemSoundsSession();

        [PreserveSig]
        int SetDuckingPreference(bool optOut);
    }

    [Guid("5CDF2C82-841E-4546-9722-0CF74078229A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IAudioEndpointVolume
    {
        [PreserveSig]
        int NotImpl1();

        [PreserveSig]
        int NotImpl2();

        [PreserveSig]
        int GetChannelCount(
            [Out][MarshalAs(UnmanagedType.U4)] out UInt32 channelCount);

        [PreserveSig]
        int SetMasterVolumeLevel(
            [In][MarshalAs(UnmanagedType.R4)] float level,
            [In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);

        [PreserveSig]
        int SetMasterVolumeLevelScalar(
            [In][MarshalAs(UnmanagedType.R4)] float level,
            [In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);

        [PreserveSig]
        int GetMasterVolumeLevel(
            [Out][MarshalAs(UnmanagedType.R4)] out float level);

        [PreserveSig]
        int GetMasterVolumeLevelScalar(
            [Out][MarshalAs(UnmanagedType.R4)] out float level);

        [PreserveSig]
        int SetChannelVolumeLevel(
            [In][MarshalAs(UnmanagedType.U4)] UInt32 channelNumber,
            [In][MarshalAs(UnmanagedType.R4)] float level,
            [In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);

        [PreserveSig]
        int SetChannelVolumeLevelScalar(
            [In][MarshalAs(UnmanagedType.U4)] UInt32 channelNumber,
            [In][MarshalAs(UnmanagedType.R4)] float level,
            [In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);

        [PreserveSig]
        int GetChannelVolumeLevel(
            [In][MarshalAs(UnmanagedType.U4)] UInt32 channelNumber,
            [Out][MarshalAs(UnmanagedType.R4)] out float level);

        [PreserveSig]
        int GetChannelVolumeLevelScalar(
            [In][MarshalAs(UnmanagedType.U4)] UInt32 channelNumber,
            [Out][MarshalAs(UnmanagedType.R4)] out float level);

        [PreserveSig]
        int SetMute(
            [In][MarshalAs(UnmanagedType.Bool)] Boolean isMuted,
            [In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);

        [PreserveSig]
        int GetMute(
            [Out][MarshalAs(UnmanagedType.Bool)] out Boolean isMuted);

        [PreserveSig]
        int GetVolumeStepInfo(
            [Out][MarshalAs(UnmanagedType.U4)] out UInt32 step,
            [Out][MarshalAs(UnmanagedType.U4)] out UInt32 stepCount);

        [PreserveSig]
        int VolumeStepUp(
            [In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);

        [PreserveSig]
        int VolumeStepDown(
            [In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);

        [PreserveSig]
        int QueryHardwareSupport(
            [Out][MarshalAs(UnmanagedType.U4)] out UInt32 hardwareSupportMask);

        [PreserveSig]
        int GetVolumeRange(
            [Out][MarshalAs(UnmanagedType.R4)] out float volumeMin,
            [Out][MarshalAs(UnmanagedType.R4)] out float volumeMax,
            [Out][MarshalAs(UnmanagedType.R4)] out float volumeStep);
    }
}

音量を変更するコード

こんな感じになる。

public static void SetMasterVolume(float newLevel)
{
    IAudioEndpointVolume masterVol = null;

    try
    {
        masterVol = GetMasterVolumeObject();

        if (masterVol == null)
        {
            return;
        }

        masterVol.SetMasterVolumeLevelScalar(newLevel / 100, Guid.Empty);
    }
    finally
    {
        if (masterVol != null)
        {
            Marshal.ReleaseComObject(masterVol);
        }
    }
}

private static IAudioEndpointVolume GetMasterVolumeObject()
{
    IMMDeviceEnumerator deviceEnumerator = null;
    IMMDevice speakers = null;

    try
    {
        deviceEnumerator = (IMMDeviceEnumerator)(new MMDeviceEnumerator());
        deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out speakers);

        Guid IID_IAudioEndpointVolume = typeof(IAudioEndpointVolume).GUID;

        speakers.Activate(ref IID_IAudioEndpointVolume, 0, IntPtr.Zero, out object endpointVolume);
        IAudioEndpointVolume masterVol = (IAudioEndpointVolume)endpointVolume;

        return masterVol;
    }
    finally
    {
        if (speakers != null) Marshal.ReleaseComObject(speakers);
        if (deviceEnumerator != null) Marshal.ReleaseComObject(deviceEnumerator);
    }
}

使うときはこんな感じ。

var newVolume = (float)0100;

SetMasterVolume(newVolume);

サンプル

GitHub にサンプルを公開しています。サンプルでは、ミュートにも対応。

f:id:shikaku_sh:20210727171218p:plain:w600

参考

WPF + .NET 5 で CsWin32metadata を利用するサンプル

.NET 5 環境で Win 32 API を利用しようと思ったら、従来なら P/Invoke(DllImport)で利用する関数や構造体なんかを再定義して利用していたと思います。2021 年末を完成目標にして「win32metadata」が立ち上がっているので、こっちを利用してみるテスト。

win32metadata は、Win 32 API を定義したメタデータなので、それを呼び出すためには、次の拡張を利用するという関係になっている。

このあたりについて、実際にコードとして、ウマ娘のウィンドウ画像をキャプチャーするサンプルを書いてみたのでメモ。

使い方のメモ

  1. プロジェクトに「NativeMethods.txt」テキストファイルを追加する。
  2. 「NativeMethods.txt」テキストファイルの記述ルールは次のとおり:
  3. 必要な関数名(例:FindWindow)(必要に応じて、A, W のサフィックスが含まれることがある)を記入する。
  4. モジュール名の後に .* をつけると、モジュールのメソッドが全部生成される。(例:Kernel32.*)
  5. 生成することができるものは、struct, enum, 定数、または、インターフェースの名前です。
  6. コメント (//) から始まる行、空白行は無視する。

テキストファイルに追加した関数は、次のコードで利用します:

using Microsoft.Windows.Sdk;

// 関数呼び出し
PInvoke.追加した関数名(/*args*/);

// 定数呼び出し
Constants.追加した定数名

「NativeMethods.txt」テキストファイルをプロジェクトに追加していないとき、または、内容が空白のとき、Microsoft.Windows.CsWin32 はコードをジェネレートしていないみたいです。このときは、名前空間 Microsoft.Windows.Sdk定義されていません。 なので「NativeMethods.txt」を編集する前にコーディングをすると、必ずエラーになります。注意しましょう。

WPF 環境の場合(コンパイルエラーになる問題)

WPF + .NET 5 環境だと 2021 年 6 月時点、バグがあってコードインテリジェンスで記述している時点では、ジェネレーターが追加した win32 用の関数等を正常に読み込めているけど、コンパイル時は解決できずに失敗します。

この問題は、以下で修正案が指摘されていました。

プロジェクトの .csproj ファイルを開いて、以下のコードを追加します。

<IncludePackageReferencesDuringMarkupCompilation>true</IncludePackageReferencesDuringMarkupCompilation>
> <UseWPF>true</UseWPF>

PropertyGroup の中に加えます。(UseWPF は最初から記述されているかも)サンプルは以下のとおりです。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net5.0-windows</TargetFramework>
    <IncludePackageReferencesDuringMarkupCompilation>true</IncludePackageReferencesDuringMarkupCompilation>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Windows.CsWin32" Version="0.1.422-beta">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="System.Drawing.Common" Version="5.0.2" />
  </ItemGroup>

</Project>

サンプル

GitHub に「CoreAndWin32Test」のプロジェクトを追加しています。テストコードでは、メモ帳のウィンドウコピー、DMM 版ウマ娘のウィンドウコピーを実装してみました。

NativeMethods に追加した関数は次のとおりです。

BITMAPINFO
BITMAPINFOHEADER

BitBlt
CreateCompatibleDC
CreateDIBSection
FindWindow
GetClientRect
GetWindowDC
MapWindowPoints
ReleaseDC
SelectObject

こんな感じで、ウィンドウの枠を除いた画像をコピーすることができます。

f:id:shikaku_sh:20210607131300p:plain:w300
サンプル

個人的な意見

NativeMethods に登録した関数 FindWindow だと、HWND のような構造体のようなクラスも自動的にコンパイルされるようになります。 ただ、このあたりは win32 API を知らない人だと、単に IntPtr で操作したほうが楽だったりもすると思うので、便利なんだけど型を具体的にする一方で面倒もあるなという感じでした。(ポインタを使わざるを得なくなるところもあって、unsafe コードになったりもしたし)

参考

C# DMM 版ウマ娘のウィンドウ画像をキャプチャー保存する (+OCR メモ)

DMM 版のウマ娘のウィンドウ画像をキャプチャーをして、あれこれサポートするツールでは「UmaUmaCruise」が有名です。

で、このツールがどうやってウマ娘の画面をキャプチャーしているのかな、って調べたところ C++ のコーディングだったので、C# に置き換えつつ、キャプチャーできるかなーってのをやってみた。

以下、うまくできたので公開。

指定したウィンドウ(DMM 版ウマ娘)を取得する

C++ コードの真似なので、Win32 API をたくさん呼び出すことになりました。なので、DLL Import をたくさん使います。

ウィンドウの取得は FindWindow を使います。

[DllImport("user32.dll")]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

public static IntPtr GetHandle(string className, string windowName)
{
    var handle = FindWindow(className, windowName);

    return handle;
}

DMM 版ウマ娘のハンドル IntPtr を取得するときはこうする。

var handle = GetHandle("UnityWndClass", "umamusume");

ハンドルからウィンドウの表示画像を取得する

取得する方法は、ざっくり2通りあって次のとおり:

  1. PC の画面全体をキャプチャー、指定ウィンドウの表示してる部分を切り取ってコピーする
  2. PrintWindow 関数からウィンドウの表示画面をコピーする

できることなら「2」を採用したいんだけど、DMM 版ウマ娘はこの方法だとキャプチャー画像が正しいものにならない(ピンク色の画面になる)ので、1を採用するみたいです。

ここで使用する DLL Import は次の関数:

[DllImport("user32.dll")]
static extern int GetClientRect(IntPtr hWnd, out RECT lpRect);

[DllImport("user32.dll", ExactSpelling = true, SetLastError = true)]
static extern int MapWindowPoints(IntPtr hWndFrom, IntPtr hWndTo, [In, Out] ref RECT rect, [MarshalAs(UnmanagedType.U4)] int cPoints);

[DllImport("user32.dll")]
static extern IntPtr GetWindowDC(IntPtr hwnd);

[DllImport("gdi32.dll")]
static extern IntPtr CreateDIBSection(IntPtr hdc, [In] ref BITMAPINFO pbmi, uint pila, out IntPtr ppvBits, IntPtr hSection, uint dwOffset);

[DllImport("gdi32.dll")]
static extern IntPtr CreateCompatibleDC(IntPtr hdc);

 [DllImport("gdi32.dll")]
static extern IntPtr SelectObject(IntPtr hDC, IntPtr hObject);

DLL Import で使われたりしている構造体と列挙子も定義する:

enum BitmapCompressionMode : uint
{
    BI_RGB = 0,
    BI_RLE8 = 1,
    BI_RLE4 = 2,
    BI_BITFIELDS = 3,
    BI_JPEG = 4,
    BI_PNG = 5
}

[StructLayout(LayoutKind.Sequential)]
struct RECT
{
    public int left;
    public int top;
    public int right;
    public int bottom;
}

[StructLayout(LayoutKind.Sequential)]
struct BITMAPINFOHEADER
{
    public uint biSize;
    public int biWidth;
    public int biHeight;
    public ushort biPlanes;
    public ushort biBitCount;
    public BitmapCompressionMode biCompression;
    public uint biSizeImage;
    public int biXPelsPerMeter;
    public int biYPelsPerMeter;
    public uint biClrUsed;
    public uint biClrImportant;

    public void Init()
    {
        biSize = (uint)Marshal.SizeOf(this);
    }
}

[StructLayout(LayoutKind.Sequential)]
struct BITMAPINFO
{
    public BITMAPINFOHEADER bmiHeader;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1024)]
    public byte[] bmiColors;
}

肝心の BitMap 画像として保存するのはこんな感じ:

using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media.Imaging;

public static Bitmap GetCaptureImage(IntPtr handle)
{
    int result = 0;

    IntPtr desktopDC = IntPtr.Zero;
    IntPtr memoryDC = IntPtr.Zero;
    Bitmap bitmap = null;

    try
    {
        result = GetClientRect(handle, out RECT rect);

        if (result == 0) throw new NullReferenceException($"指定したハンドルのウィンドウ({handle})を発見することができませんでした。");

        result = MapWindowPoints(handle, IntPtr.Zero, ref rect, 2);

        if (result == 0) throw new NullReferenceException($"指定したハンドルのウィンドウ({handle})の座標空間の変換に失敗しました。");
        
        var tempRect = rect;

        desktopDC = GetWindowDC(IntPtr.Zero); // デスクトップの DC を取得
        
        var header = new BITMAPINFOHEADER()
        {
            biSize = (uint)Marshal.SizeOf(typeof(BITMAPINFOHEADER)),
            biWidth = tempRect.right - rect.left,
            biHeight = tempRect.bottom - rect.top,
            biPlanes = 1,
            biCompression = BitmapCompressionMode.BI_RGB,
            biBitCount = 24,
        };

        var info = new BITMAPINFO
        {
            bmiHeader = header,
        };

        var hBitmap = CreateDIBSection(desktopDC, ref info, DIB_RGB_COLORS, out _, IntPtr.Zero, 0);
        
        memoryDC = CreateCompatibleDC(desktopDC);

        var phBitmap = SelectObject(memoryDC, hBitmap);

        BitBlt(memoryDC, 0, 0, header.biWidth, header.biHeight, desktopDC, rect.left, rect.top, SRCCOPY);

        SelectObject(memoryDC, phBitmap);

        bitmap = Bitmap.FromHbitmap(hBitmap, IntPtr.Zero);
    }
    finally
    {
        if (desktopDC != IntPtr.Zero)
        {
            ReleaseDC(IntPtr.Zero, desktopDC);
        }

        if (memoryDC != IntPtr.Zero)
        {
            ReleaseDC(handle, memoryDC);
        }
    }

    if (bitmap == null) return null;

    return bitmap;
}

ウマ娘のウィンドウ画像を保存するコードは、こんな感じ:

var handle = WindowsAPI.GetHandle("UnityWndClass", "umamusume");
var bitmap = WindowsAPI.GetCaptureImage(handle);

bitmap?.Save("test.png", System.Drawing.Imaging.ImageFormat.Png);

f:id:shikaku_sh:20210607131300p:plain:h400
こんな感じでできた

ここから、UmaUmaCruise は Tesseract OCR ライブラリーを使って画像解析、テキストを抽出して、テキストを SimString で評価して、イベントタイトルの中で一番類似するものを表示しているのだと思います。かしこい!

Tesseract OCR は読み取り精度があまり完璧な感じではないので、SimString で一致してるっぽいな、でやったのがシンプルにえらいと思います。

Tesseract OCR の訓練済モデルもおそらく、GitHub 版じゃなくて、精度がよいらしい Ubuntu 版を使用してるっぽい。

画像の切り出し

これが一番らくだと思います。

// 切り出しテスト
var snipBitmap = bitmap?.Clone(new Rectangle(x, y, width, height), bitmap.PixelFormat);

x, y の位置から width, height の領域を切り出す。切り出した領域を拡大縮小するときに使うかもしれません。OCR はすこし画像サイズが大きいくらいが有利なことも。

OCR にかけるとき

たぶんこんな感じ:

using (var tesseract = new Tesseract.TesseractEngine(System.IO.Path.Combine(exePath, "tessdata"), "jpn"))
{
    tesseract.SetVariable("tessedit_char_whitelist", "0123456789");

    var pix = PixConverter.ToPix(bitmap);

    // ここで領域を指定することも可能
    var p = tesseract.Process(pix, PageSegMode.SingleLine or SingleWord);

    Console.WriteLine(p.GetText());
}

tesseract 自体は NuGet で。

既存の学習データは tesseract-ocr-jpn がよさそう。

サンプル

GitHub にサンプルコードを公開しています。

現在ステータスの合計値とかを表示したいなーと思ったんだけど、これは読み取り精度がカギになってくるので、言語データの再学習とかも必要になってきそう。根気がたりん!

参考

PowerPoint スライドの画像出力サイズを変更する方法

PowerPoint のスライドを PNG 形式の画像ファイルとして保存するときの画像サイズを変更する方法をメモします。

f:id:shikaku_sh:20210607103304p:plain:w600
これ

変更の手順

  1. Win + R で「ファイル名を指定して実行」を起動
  2. regedit
  3. 下表から対応する Power Point のレジストリサブキーを検索
  4. サブキーから「DWORD (32 bit) 値」を追加
    • 名前:「ExportBitmapResolution」
    • 値:10 進数で「300」だと最大サイズ(4000x2250)
Version Path
2016 2019 365 HKEY_CURRENT_USER\Software\Microsoft\Office\16.0\PowerPoint\Options
2013 HKEY_CURRENT_USER\Software\Microsoft\Office\15.0\PowerPoint\Options
2010 HKEY_CURRENT_USER\Software\Microsoft\Office\14.0\PowerPoint\Options
2007 HKEY_CURRENT_USER\Software\Microsoft\Office\12.0\PowerPoint\Options
2003 HKEY_CURRENT_USER\Software\Microsoft\Office\11.0\PowerPoint\Options

f:id:shikaku_sh:20210607103626p:plain:w500 f:id:shikaku_sh:20210607103634p:plain

参考

GAS (Google Apps Script) を使って、フォルダーの更新検知と自動メール通知をする

f:id:shikaku_sh:20210524153300p:plain

古来からのプログラマーは、Excel VBA を覚える気がなくても日本の社会通念より必ず身につけてしまうものでした。現在でも ExcelVBA を使うケースがちょこっとあるんですが「自動化」をしたいときは Google Apps Script のほうがいいよな、と改めて実感する機会に恵まれたのでメモ。

特定のフォルダーないに、次のアクションがあったとき:

  • フォルダーが作成される
  • すでに存在するフォルダーの更新日時が更新される

自動的にメールを送信して更新をチームに周知させる、という目的です。なんかのデータが公開された、というような通知を自動化したかったのです。

「自動的に」というのがポイントになっていて、Excel + VBAExcel ファイルを開かないと(基本的に)プログラムが走りません。なんで、勝手にやっててください、という状態を構築するのは得意ではありません。Google SpreadSheet はクラウドの特性を持つので(わりかし)得意かも。

Excel + Power Automate で組み合わせる、または、Power Automate で完結しているというのも今だったらありえますが、ちょっとした 100 行程度のコードで走るプログラムは、やっぱり VBA や GAS が便利に思えるのです。シンプルな構成で完結する、という強みのためです。

Spread Sheet と GAS

Google Spread Sheet は Web アプリケーションです。ファイルとして、存在していても保存されている情報は、(メモ帳で開くとわかりますが)次の3つだけ:

  • url
  • doc_id
  • email

このため、基本的には Google Spread Sheet は Google Drive 内にしか生息できません。Google Drive に実体がある、という認識でよいと思います。これは Google Document もそうだし、基本的なルールになっています。

はてな にて gas で記述したコードブロックを markdownシンタックスハイライトにするときは、「Creating and highlighting code blocks 」を参照するも適切なものはわからなかったです。javascript で仮対応。

コーディング

正直、GAS の言語仕様を私は、全然まったく調べていないです。サンプルコードも冗長なシート取得を繰り返しているので、実効速度の観点でみると効率的ではないです。

ざっくりとしたことだけメモ。

スクリプトエディターを開くとき

どこを開くんだっけ、ってなるけど:

f:id:shikaku_sh:20210524154028p:plain:w500

デバッグを有効化する

デバッグが起動できるか、ブレークポイントが有効か、単純なサンプルコードでテストする。

function Debug()
{
  Logger.log("test");
}

実行ログは Ctrl + Enter で表示する。ただし、エディターにフォーカスがあるときは機能しないショートカットなので、フォーカスをメニューなどに移してからショートカットキーを利用する。

初回実行時は、「承認」が必要になることがあります。

f:id:shikaku_sh:20210524152704p:plain:w600 f:id:shikaku_sh:20210524152707p:plain:w600

サンプルコード

単純なコードなんで、一度も GAS に触れたことがなくても理解できると思います。

var TargetFolderID = "フォルダーのID";

var SpreadSheetID = "スプレッドシートのID";
var LoggingSheetName = "スプレッドシートのシート名";

var ToMailAddresses = ["メールアドレス 1", "メールアドレス 2"];

function UpdateLog()
{
    var notifications = WriteLog_(TargetFolderID, SpreadSheetID, LoggingSheetName);

    if (notifications.length > 0)
    {
       SendMail_(ToMailAddresses, notifications);
    }
}

function SendMail_(to, notifications)
{
    var titles = "";

    for (var i in notifications)
    {
        var notification = notifications[i];

        if (titles != "")
        {
            titles += "\r\n";
        }

        titles += notification.name;
    }

    var body = "指定したフォルダーが更新されました。\r\n" +
               "更新されたフォルダーは次のフォルダーです。\r\n\r\n" +
               "--\r\n" +
               titles + 
               "\r\n" +
               "--\r\n\r\n" +
               "このメールは送信専用です。返信しても確認できません。ご了承ください。";

    for (var i in to)
    {
        var mail = to[i];

        MailApp.sendEmail(mail, "指定フォルダー更新 連絡", body);
    }
}

function WriteLog_(folderID, sheetID, sheetName) 
{
    var folders = GetFoldersById_(folderID);
    var logs = GetSheetLogsByIdAndName_(sheetID, sheetName);

    var spreadSheet = SpreadsheetApp.openById(sheetID);
    var sheet = spreadSheet.getSheetByName(sheetName);

    var notifications = [];

    for (folderTitle in folders)
    {
        var selectedFolder =  folders[folderTitle];

        if (selectedFolder.name in logs)
        {
            // フォルダー名がシートに存在するときは、更新日時を確認して更新日時が更新されたとき対象
            if (selectedFolder.lastUpdated > logs[selectedFolder.name].lastUpdated)
            {
                sheet.getRange(logs[selectedFolder.name].rowNo, 2).setValue(logs[selectedFolder.name].lastUpdated);
                notifications.push(selectedFolder);
            }
        }
        else 
        {
            var lastRow = sheet.getLastRow();

            // フォルダー名がシートに存在しないときは、新規の通知対象
            sheet.getRange(lastRow + 1, 1).setValue(selectedFolder.name);
            sheet.getRange(lastRow + 1, 2).setValue(selectedFolder.lastUpdated);
            notifications.push(selectedFolder);
        }
    }

    return notifications;
}

function GetFoldersById_(id)
{
    var targetFolder = DriveApp.getFolderById(id);
    var folders = targetFolder.getFolders();
    var files = targetFolder.getFiles();

    var lastUpdateMap = {};
    var i = 1;

    // 指定したフォルダー内のフォルダーの名前と最終更新日を取得
    while (folders.hasNext())
    {
        var folder = folders.next();

        lastUpdateMap["folder_" + i++] = { name:folder.getName(), lastUpdated:folder.getLastUpdated() };
    }

    return lastUpdateMap;
}

function GetSheetLogsByIdAndName_(id, name)
{
    var spreadSheet = SpreadsheetApp.openById(id);
    var sheet = spreadSheet.getSheetByName(name);
    var values = sheet.getDataRange().getValues();

    var logs = {};

    for (var i=1; i < values.length; i++)
    {
        logs[values[i][0]] = { name:values[i][0], lastUpdated:values[i][1], rowNo:i+1 };
    }

    return logs;
}

関数名の最後に _ をつけることで、プライベート関数として定義することができます。関数内の関数定義もいいけど、古式ゆかしい。

トリガーを設定する

作成した関数を適当なタイミングで自動実行するために、トリガーを作成します。このトリガー機能が Excel には組み込みづらいので、Google SpreadSheet の特徴になっていると思います。

f:id:shikaku_sh:20210524152841p:plain:w500 f:id:shikaku_sh:20210524152845p:plain:w500

参考

ブルーライトカット眼鏡について一次資料を確認した、まとめ

f:id:shikaku_sh:20210416164204p:plain
ブルーライトカット眼鏡

ブルーライトカット」の記事について(個人的に)調べたことをまとめた記事です。

今日、話題になっている根拠のもとは日本眼科医会の「小児のブルーライトカット眼鏡装用に対する慎重意見」がスタートみたいです。

なんで、意見書の参考文献(一次資料)をひとつずつ読んでいった内容をメモしています。

(かつて)ブルーライトカット推奨だった理由と、それ意味ないじゃんという話

一応、昨日までは、ブルーライトが疲れを引き起こす原因は可視光の中でも波長が短く、人体に有害な紫外線に近いから、というのが定説だったらしいです。

ブルーライトを削減する眼鏡で大儲けした JINS記事で「あぶない!」って説明があったんだけど、今日は「網膜に障害を生じることはないレベルであり、いたずらにブルーライトを恐れる必要はない」という結果を引用しています。(野外活動と近視の関連性についての論文「The Role of Time Exposed to Outdoor Light for Myopia Prevalence and Progression」)

f:id:shikaku_sh:20210416164334p:plain
ちなみにページ削除+検証結果が途中で削除されています

論文中では、近視になるのと野外活動の時間の増加で、近視の発症率等の低下と関連性を示すデータが増えていて、屋外での時間を増やすことで近視進行を遅らせる示唆まで書かれています。(JINS どうして)

どうしてブルーライトカット非推奨になったか

「屋外セーフはわかったけど、ブルーライトはカットしてても別にいいじゃないか」に、なぜならなかった(受容したほうがよい)のか。

これは、「睡眠ホルモンであるメラトニンの分泌調整である神経節細胞がブルーライトを受容することで行なわれていることが発見された」ことが、「心身の発育に好影響を与えるもの」にも繋がっているのかなぁ、と個人的に思いました。(住宅照明中のブルーライトが体内時計と睡眠覚醒に与える影響 P92)

意見書の中で一番の問題提起になるブルーライト被爆よりもブルーライトカットが有害の可能性を示唆する参考文献である Northern CA Retina Vitreous Assoc の Rahul Khurana, MD さんの記事「Are Blue Light-Blocking Glasses Worth It?」では、「アカデミーではコンピューター用の特別なアイウェアを推奨していません」とありました。

There is no scientific evidence that the light coming from computer screens is damaging to the eyes. Because of this, the Academy does not recommend any special eye wear for computer use.

コンピューターを長時間利用していると目が疲れるのは、まばたきの回数が減ることが原因で眼精疲労になるとしていて、こまめに画面から離れましょうと。(また、ブルーライトカット眼鏡は眼精疲労の症状を改善しない結果が示唆されている)で、コンピューターの光が原因で目の病気になるという証明結果はありませんとなっています。一方で、ブルーライトが体内のサーカディアンリズム(自然な目覚めと睡眠のサイクル)に影響を与えるという証拠はいくつかあるので、睡眠の妨げになる睡眠2~3時間前は、画面を使わないことが一番で、バイスの「ダーク/ナイト」モードの利用も効果的としています。なので、やっぱりサーカディアンリズムがポイントになるみたいですね。

眼精疲労を和らげるためには

  • コンピューターの画面から25インチ(63.5 cm)離れて座る。(腕の長さくらい)
    • 画面をすこし下に向けてみる
  • 目が乾いていると思ったら、目薬を点眼する
  • 部屋の照明を調節する
    • スクリーンのコントラストを上げる
    • マットスクリーンフィルターを利用する
  • コンタクトレンズを使用しているときは、眼鏡をかけて目を休ませる

参考文献の「Are Blue Light-Blocking Glasses Worth It?」より

個人的なまとめ

ブルーライトカットは万能な正解ではないとわかった。しかし、すべての場合において、否定されたわけじゃないことがわかった。

ブルーライトカット眼鏡を常時かけるのは極端な対策で、サーカディアンリズムに悪影響を及ぼす恐れがあることがわかった。

一方で、ブルーライトは眼表皮に対する光毒性があることも認められているらしい。ブルーライトによって、高齢者、コンタクトレンズをつけている人、寝不足、栄養不足等のコンディションが良好でないときは、急性または慢性眼表皮異常のリスクがある。

また、夜間のブルーライトには覚醒作用がある。ブルーライトを遮光することで眠気が優位に増加した結果がある。屋内照明は、ブルーライト対策をとることで、特に夜間の健康被害を回避できる。

なので、ブルーライトカットが有効なことも(まだ)ありそう……というか、窓、カーテン、遮光、照明を正しい時間にあわせることで、体内時計を調節することが望ましい。色彩を勉強して(興味をもてて)よかった。

あと、日本眼科医会は、参考になる PDF をこまめにアップロードしてるみたいです。「デジタル機器により生じる視機能の弊害」、「ドライアイへの対応と治療」も参考になる内容でした。RSSTwitter に対応さえしていれば……。

参考

三体

三体

C# .Net Framework と Visual C 再配布パッケージのインストール確認

いまでも WPFWindows ネイティブアプリケーションを作るときに .NET Framework を使うことがあるのですが、古い PC やセットアップ不足の PC でうまく動かないことがありました。

発生したことのある原因は(だいたい)これ:

ちなみに、.NET Framework はバージョン 4.8 を最後にして、メジャーアップデートを終了することがアナウンスされています。今後は .NET Core になるはずです。

基本的にプラットフォームが Windows に限定された .NET Framework なんですが、そのわりにインストールされているバージョン情報をパッと確認(しづらかったり)できなかったり、同様に「Visual Studio 20XX Visual C++ 再頒布可能パッケージ」もインストールされているのかどうかわかりづらいので、とりあえずインストールの実行をしたという経験があるかもしれません。

このあたりの確認方法をメモ。

.NET Framework のバージョン確認

Microsoft は、バージョン情報の確認方法を「Microsoft Docs - How to: Determine which .NET Framework versions are installed」でまとめています。

レジストリー領域の話になるので、すぐにめんどくさいことになったと気づきます。しかも、パッとわからない。統一的な記録形式を守ってくれていないので、さらにめんどくさい。

f:id:shikaku_sh:20210406105222p:plain:w300 f:id:shikaku_sh:20210406105225p:plain:w300 f:id:shikaku_sh:20210406105228p:plain:w300 f:id:shikaku_sh:20210406105232p:plain:w300

取得するサンプルコードも併せて載せてありますが、コレクションで取得するようにすると、こんな感じ:

using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PackageVersionChecker
{
    /// <summary>
    /// <see cref="DotNetVersion"/> クラスは、.NET のバージョンを確認するためのクラスです。
    /// </summary>
    public class DotNetVersion
    {
        /// <summary>
        /// どの .NET Framework バージョンがインストールされているか登録情報のコレクションを取得します。
        /// </summary>
        /// <returns>登録情報の組のコレクション (<see cref="Version"/>, SP)。登録情報が存在しないとき null を返却します。</returns>
        public static IEnumerable<Tuple<Version, string>> GetVersions()
        {
            var versions = new List<Tuple<Version, string>>();

             var v1 = GetVersion1To45FromRegistry();
             var v2 = GetVersionOver45FromRegistry();

            if (v1 != null)
            {
                versions.AddRange(v1);
            }

            if (v2 != null)
            {
                versions.Add(v2);
            }

            return versions.Count > 0 ? versions : null;
        }

        #region Public Methods

        /// <summary>
        /// .NET Framework 1 - 4.0 までのバージョンがインストールされているかどうかを示す登録情報を取得します。
        /// </summary>
        /// <returns>登録情報の組のコレクション(バージョン、SP)。登録情報が存在しないとき null を返却します。</returns>
        public static IEnumerable<Tuple<Version, string>> GetVersion1To45FromRegistry()
        {
            const string reg = @"SOFTWARE\Microsoft\NET Framework Setup\NDP\";

            var netVersions = new List<Tuple<Version, string>>();

            using (var key = Registry.LocalMachine.OpenSubKey(reg))
            {
                foreach (var keyName in key.GetSubKeyNames())
                {
                    var version = "";
                    var sp = "";
                    var install = "";

                    if (keyName == "v4") continue;
                    if (!keyName.StartsWith("v")) continue;

                    var versionKey = key.OpenSubKey(keyName);

                    version = versionKey.GetValue("Version", "").ToString();
                    sp = versionKey.GetValue("SP", "").ToString();
                    install = versionKey.GetValue("Install", "").ToString();

                    // Version 2-3.5 対応
                    if (!string.IsNullOrEmpty(version))
                    {
                        if (!(string.IsNullOrEmpty(sp)) && install == "1")
                        {
                            netVersions.Add(Tuple.Create(new Version(version), sp));
                        }
                        else
                        {
                            netVersions.Add(Tuple.Create(new Version(version), ""));
                        }

                        continue;
                    }

                    // Version 4.0 対応
                    foreach (var subKeyName in versionKey.GetSubKeyNames())
                    {
                        var subKey = versionKey.OpenSubKey(subKeyName);

                        version = subKey.GetValue("Version", "").ToString();

                        if (!string.IsNullOrEmpty(version))
                        {
                            sp = subKey.GetValue("SP", "").ToString();
                        }

                        install = subKey.GetValue("Install").ToString();

                        if (string.IsNullOrEmpty(install))
                        {
                            netVersions.Add(Tuple.Create(new Version(version), ""));
                        }
                        else
                        {
                            if (!(string.IsNullOrEmpty(sp)) && install == "1")
                            {
                                netVersions.Add(Tuple.Create(new Version(version), sp));
                            }
                            else
                            {
                                netVersions.Add(Tuple.Create(new Version(version), ""));
                            }
                        }
                    }

                }

                return netVersions.Count > 0 ? netVersions : null;
            }

        }

        /// <summary>
        /// .NET Framework 1 - 4.0 までのバージョンがインストールされているかどうかを示す登録情報を取得します。
        /// </summary>
        /// <returns>登録情報の組(バージョン、SP)。登録情報が存在しないとき null を返却します。</returns>
        public static Tuple<Version, string> GetVersionOver45FromRegistry()
        {
            const string reg = @"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\";

            using (RegistryKey ndpKey = Registry.LocalMachine.OpenSubKey(reg))
            {
                if (ndpKey == null) return null;

                var version = ndpKey.GetValue("Version")?.ToString() ?? "";

                if (!string.IsNullOrEmpty(version))
                {
                    return Tuple.Create(new Version(version), "");
                }
                else
                {
                    var result = int.TryParse(ndpKey.GetValue("Release")?.ToString() ?? "", out int release);

                    if (result)
                    {
                        var versionText = ToVersion(release);

                        if (!string.IsNullOrEmpty(versionText))
                        {
                            return Tuple.Create(new Version(versionText), "");
                        }
                    }
                }
            }

            return null;
        }

        #endregion

        /// <summary>
        /// リリースキーの番号からバージョンの番号を取得します。
        /// </summary>
        /// <param name="release">リリースキーの番号。</param>
        /// <returns>バージョンの番号。変換に失敗したときは "" を返却します。</returns>
        private static string ToVersion(int release)
        {
            if (release >= 528040) return "4.8";
            if (release >= 461808) return "4.7.2";
            if (release >= 461308) return "4.7.1";
            if (release >= 460798) return "4.7";
            if (release >= 394802) return "4.6.2";
            if (release >= 394254) return "4.6.1";
            if (release >= 393295) return "4.6";
            if (release >= 379893) return "4.5.2";
            if (release >= 378675) return "4.5.1";
            if (release >= 378389) return "4.5";

            return "";
        }
    }
}

出力結果は、こんな感じ:

f:id:shikaku_sh:20210406105340p:plain

.NET のバージョンは、いくつかのバージョンがインストールされていることがあるので、Microsoft 公式のサンプルコードでも複数個のバージョンが出力されています。

Visual Studio 20XX Visual C++ 再頒布可能パッケージの確認

再配布パッケージはいくつかのバージョンがあります。

なので、これも .NET と同じように、インストールされている再配布パッケージを一覧するような仕様がよいと思います。やりかたは色々あると思いますが、System.Management の参照を追加してやる方法はこんな感じ:

/// <summary>
/// <see cref="WinProduct"/> クラスは、Win32_Product のデータを表現するクラスです。
/// </summary>
public class WinProduct
{
    public int No { get; set; }

    public string IdentifyingNumber { get; set; }

    public string Name { get; set; }
}
private WinProduct[] GetWinProduct(string likeName)
{
    var products = new List<WinProduct>();
    using (var searcher = new ManagementObjectSearcher($"SELECT Name, IdentifyingNumber FROM Win32_Product WHERE Name LIKE '{likeName}'"))
    {
        int no = 1;

        foreach (ManagementObject obj in searcher.Get())
        {
            var name = obj["Name"] as string;
            var identify = obj["IdentifyingNumber"] as string;

            var product = new WinProduct()
            {
                No = no++,
                Name = name,
                IdentifyingNumber = identify,
            };

            products.Add(product);

            Console.WriteLine(name);
        }
    }

    return products.ToArray();
}

likeName の引数で、取得するインストール済のアプリケーション名を指定します。今回だと、こんな感じでどうか。(%SQL におけるワイルドカード(あいまい)の表現です。)

  • %Microsoft Visual C%

%Microsoft Visual C%Redistributable% だと、2019 などの新しいパッケージは名称がランタイムになっていることもあるのでヒットしないかも。

private async void CheckRedistributablePackage()
{
    var products = await Task<WinProduct[]>.Run(() => GetWinProduct("%Microsoft Visual C%"));

    foreach (var product in products)
    {
        Console.WriteLine(product);
    }
}

このやり方には欠点があって、データ取得に数分かかってしまいます。foreach のところで、固まります。OS のアプリケーションのアンインストール一覧を表示するときでも表示に時間がかかりますが、そんな感じだと思います。

結果を出力するとこんな感じ:

f:id:shikaku_sh:20210406105411p:plain:w600

図でリスト表示してるのは、下記の自作したサンプルになります。

サンプル

今回のテストコードは GitHub に公開しています。

f:id:shikaku_sh:20210406105411p:plain:w600

おまけ(ランタイム is なに)

大丈夫だと思うけど、.NET Framework に限らず、ランタイムの意味を把握できていますか:

  • ランタイム (Rumtime)
    ソフトウェアを実行するためのもの
  • SDK (Software Development Kit)
    ソフトウェアを開発するためのもの

.NET Framework で作られていても、実行するためにはランタイムが必要だし、開発するためには SDK が必要という感じになります。

英語を確認すると意味がわかりやすいのですが、略して SDK と言われたり、カタカナでランタイムというと、初めて見聞きしたときにそのままだと用語の意味がわかりづらいかも。

開発をしてると、ランタイムエラー (Runtime Error) というエラーが発生することがありますよね。これもランタイムの意味を知っていないと、エラーの理由/原因に繋がりづらい。(カタカナ用語の無能さゆえ)

参考