VB.NETでWindows Explorerと同じ右クリックメニューを表示する方法(IContextMenu/IContextMenu2実装)

VB.NETアプリで、Windows Explorerと同じ右クリックメニュー(標準項目+圧縮ソフトなどのシェル拡張)をそのまま表示して実行したい——。その場合、.NETのContextMenuではなく、Windowsシェルが提供するCOMインターフェイス(IContextMenu/IContextMenu2)を呼び出すのが最短ルートです。この記事では、ファイルパスと表示座標からネイティブのコンテキストメニューを出す手順と、拡張メニューで必須になりやすいWndProc転送までを、VB.NETの実装パターンとして具体的にまとめます。

目次

VB.NETで「Explorerと同じ右クリックメニュー」を出すのが難しく見える理由

Windows Explorerの右クリックメニューが厄介なのは、見た目のメニュー表示よりも「中身」が多層になっている点です。Explorerのメニューは、OS標準項目(開く、送る、プロパティなど)に加えて、インストールされているアプリが登録するシェル拡張(Shell Extension)の項目も合成して作られています。

つまり、単に自分でMenuStrip/ContextMenuStripを組み立てても、以下の要件は満たせません。

  • 圧縮ソフト、Gitクライアント、クラウド同期、セキュリティ製品などが追加するサードパーティ項目まで含めたい
  • 「開く」だけでなく、各項目の実行までExplorerと同じ動作にしたい
  • オーナードロー(独自描画)やサブメニューなど、特殊なメニュー表示にも対応したい

この「Explorerがやっていること」をアプリ側で再現する正攻法が、WindowsシェルのCOMであるIContextMenu / IContextMenu2(必要ならIContextMenu3)を直接使う方法です。

結論:IContextMenu/IContextMenu2をP/Invoke+COMで直接呼び出すのが最適解

「ファイルパスと表示位置を渡したら、Explorerと同じメニューが出る」タイプの既製ライブラリは確かに存在しますが、内部でやっているのは多くの場合、まさにIContextMenu系のラップです。仕組みを理解しておくと、以下のメリットがあります。

  • メニューが出ない/一部の拡張が表示されない原因(ビット数、メッセージ転送、COM初期化など)を切り分けできる
  • 必要な機能だけを最小実装でき、依存が増えない
  • エラー時の挙動(落ちる、固まる、例外)を自分で制御しやすい

実装の全体像(使うAPIと役割)

工程目的主に使うもの
PIDL取得ファイルパスをシェルが扱うID(PIDL)へ変換ILCreateFromPath(またはSHParseDisplayName)
親フォルダへバインド親IShellFolderと子PIDL(相対)を得るSHBindToParent
IContextMenu取得対象ファイルのコンテキストメニュー生成元を取得IShellFolder.GetUIObjectOf
メニュー構築空のHMENUに項目を埋めさせるCreatePopupMenu + IContextMenu.QueryContextMenu
メニュー表示指定座標にポップアップ表示し、選択コマンドを得るTrackPopupMenu(またはTrackPopupMenuEx)
選択項目の実行Explorerと同じ動作を実行IContextMenu.InvokeCommand(CMINVOKECOMMANDINFO/EX)
描画メッセージ転送拡張メニューのオーナードロー等を崩さないIContextMenu2.HandleMenuMsg(必要ならIContextMenu3)
後始末メモリ/ハンドル/COM参照を確実に解放DestroyMenu / ILFree / Marshal.FinalReleaseComObject

実装手順をもう少し噛み砕く

PIDLとは何か(なぜ必要か)

PIDL(Pointer to an Item ID List)は、Windowsシェルがファイルやフォルダなどの「シェルアイテム」を識別するための内部表現です。Explorerのメニュー生成は、単なるファイルパス文字列ではなく、PIDLやIShellFolderを起点に行われます。そこで、まずファイルパスからPIDLを作ります。

SHBindToParentを挟む理由

コンテキストメニュー取得で重要なのは「対象アイテム(子)」だけでなく、「どのフォルダ(親IShellFolder)の中にあるアイテムか」というコンテキストです。SHBindToParentを使うと、

  • 親フォルダのIShellFolder
  • 親から見た相対PIDL(子PIDL)

が同時に取れるため、GetUIObjectOfでIContextMenuへ到達しやすくなります。

QueryContextMenuのフラグで「拡張メニュー」を含める

Explorerの「拡張メニュー」(Shiftを押しながら右クリックしたときに増える項目)相当まで含めたい場合は、QueryContextMenuにCMF_EXTENDEDVERBSを渡します。さらにExplorerに近い挙動を狙うなら、CMF_EXPLOREも合わせるのが定番です。

TrackPopupMenuの戻り値をInvokeCommandへつなぐ

TrackPopupMenu(TPM_RETURNCMD)で得られるのは、ユーザーが選択したメニュー項目のコマンドIDです。QueryContextMenuで指定したidCmdFirstを基準に、(選択ID – idCmdFirst)をlpVerbへ渡すことで、IContextMenu.InvokeCommandが正しく動きます。

拡張メニュー表示で重要:WndProcでIContextMenu2へメッセージ転送する

シェル拡張の中には、アイコン付き、特殊な余白、独自描画(オーナードロー)などを行うものがあり、これらはWin32メッセージ(WM_DRAWITEM/WM_MEASUREITEM等)を受け取って描画を成立させます。Explorerはそれを適切に拡張へ転送していますが、あなたのアプリでも同様に転送しないと、

  • メニューが真っ白になる
  • サブメニューが崩れる
  • 特定の拡張だけ表示が欠ける/落ちる

といった現象が出ます。そこで、フォームのWndProcをオーバーライドし、対象メニューを表示している間だけIContextMenu2(可能ならIContextMenu3)に転送します。

メッセージ意味転送先
WM_INITMENUPOPUPサブメニューの初期化IContextMenu2.HandleMenuMsg
WM_MEASUREITEMオーナードロー項目のサイズ計測IContextMenu2.HandleMenuMsg
WM_DRAWITEMオーナードロー項目の描画IContextMenu2.HandleMenuMsg
WM_MENUCHAR / WM_MENUGETOBJECTキーボード操作や高度なメニュー処理可能ならIContextMenu3.HandleMenuMsg2

VB.NET実装例:Explorerと同じコンテキストメニューを表示・実行する

ここでは、WinFormsを想定した実装パターンを示します(WPFでも、HwndSourceのメッセージフックで同じ考え方が使えます)。ポイントは、

  • メニュー表示中だけIContextMenu2/3参照を保持する
  • WndProcで必要なメッセージを転送する
  • PIDL/メニュー/COM参照を確実に解放する

の3つです。

フォーム側(WndProc転送+右クリックで表示)

' ※HTML貼り付け用に < > をエスケープしています
Option Strict On
Option Explicit On

Imports System.Windows.Forms

Public Class MainForm
Private ReadOnly _shellMenu As New ShellContextMenu()


Protected Overrides Sub WndProc(ByRef m As Message)
    ' 拡張メニューの描画/初期化メッセージをシェルへ転送
    If _shellMenu.HandleMenuMessage(m) Then
        Return
    End If
    MyBase.WndProc(m)
End Sub

Private Sub MainForm_MouseUp(sender As Object, e As MouseEventArgs) Handles Me.MouseUp
    If e.Button <> MouseButtons.Right Then Return

    ' 例:任意のファイル(実際は選択中のファイルパスを渡す)
    Dim target As String = "C:\Windows\System32\notepad.exe"

    ' 画面座標で表示(Cursor.PositionでもOK)
    Dim p = Cursor.Position
    _shellMenu.ShowForFile(target, Me.Handle, p.X, p.Y)
End Sub


End Class

ShellContextMenuクラス(IContextMenu/IContextMenu2を使う本体)

' ※HTML貼り付け用に < > をエスケープしています
Option Strict On
Option Explicit On

Imports System
Imports System.Runtime.InteropServices
Imports System.Windows.Forms

Public NotInheritable Class ShellContextMenu


' メニュー表示中だけ保持(WndProcで転送するため)
Private _cm2 As IContextMenu2 = Nothing
Private _cm3 As IContextMenu3 = Nothing

' --- Win32 messages ---
Private Const WM_NULL As UInteger = 0UI
Private Const WM_DRAWITEM As UInteger = &H2BUI
Private Const WM_MEASUREITEM As UInteger = &H2CUI
Private Const WM_INITMENUPOPUP As UInteger = &H117UI
Private Const WM_MENUCHAR As UInteger = &H120UI
Private Const WM_MENUGETOBJECT As UInteger = &H124UI

Private Const SW_SHOWNORMAL As Integer = 1

Public Function HandleMenuMessage(ByRef m As Message) As Boolean
    ' IContextMenu3が取れる場合は、より広いメッセージを処理可能
    If _cm3 IsNot Nothing Then
        Dim result As IntPtr = IntPtr.Zero
        Dim hr As Integer = _cm3.HandleMenuMsg2(CUInt(m.Msg), m.WParam, m.LParam, result)
        If hr = 0 Then
            m.Result = result
            Return True
        End If
    End If

    If _cm2 Is Nothing Then Return False

    Select Case CUInt(m.Msg)
        Case WM_INITMENUPOPUP, WM_DRAWITEM, WM_MEASUREITEM
            Dim hr As Integer = _cm2.HandleMenuMsg(CUInt(m.Msg), m.WParam, m.LParam)
            Return (hr = 0)
        Case WM_MENUCHAR, WM_MENUGETOBJECT
            ' IContextMenu2ではカバーしきれないことがある(拡張次第)
            Return False
    End Select

    Return False
End Function

Public Sub ShowForFile(filePath As String, ownerHwnd As IntPtr, x As Integer, y As Integer)
    If String.IsNullOrWhiteSpace(filePath) Then Return

    Dim pidlAbs As IntPtr = ILCreateFromPathW(filePath)
    If pidlAbs = IntPtr.Zero Then Return

    Dim parent As IShellFolder = Nothing
    Dim pidlChild As IntPtr = IntPtr.Zero

    Dim pUnkContextMenu As IntPtr = IntPtr.Zero
    Dim cm As IContextMenu = Nothing
    Dim hMenu As IntPtr = IntPtr.Zero

    Try
        ' 親IShellFolder + 子PIDL(相対)を取得
        Dim iidIShellFolder As Guid = GetType(IShellFolder).GUID
        Dim hr As Integer = SHBindToParent(pidlAbs, iidIShellFolder, parent, pidlChild)
        If hr <> 0 Then Marshal.ThrowExceptionForHR(hr)

        ' IContextMenuを取得
        Dim apidl() As IntPtr = {pidlChild}
        Dim iidIContextMenu As Guid = GetType(IContextMenu).GUID

        hr = parent.GetUIObjectOf(ownerHwnd, 1UI, apidl, iidIContextMenu, IntPtr.Zero, pUnkContextMenu)
        If hr <> 0 OrElse pUnkContextMenu = IntPtr.Zero Then Marshal.ThrowExceptionForHR(hr)

        cm = CType(Marshal.GetObjectForIUnknown(pUnkContextMenu), IContextMenu)
        ' RCWが参照を持つので、元ポインタはRelease
        Marshal.Release(pUnkContextMenu)
        pUnkContextMenu = IntPtr.Zero

        ' 描画対応(可能ならIContextMenu3)
        _cm2 = TryCast(cm, IContextMenu2)
        _cm3 = TryCast(cm, IContextMenu3)

        ' メニュー生成
        hMenu = CreatePopupMenu()
        If hMenu = IntPtr.Zero Then Throw New System.ComponentModel.Win32Exception()

        Const idCmdFirst As UInteger = 1UI
        Const idCmdLast As UInteger = &H7FFFUI

        Dim flags As CMF = CMF.EXPLORE Or CMF.EXTENDEDVERBS
        Dim qcm As Integer = cm.QueryContextMenu(hMenu, 0UI, idCmdFirst, idCmdLast, flags)
        If qcm < 0 Then Marshal.ThrowExceptionForHR(qcm)

        ' メニューが即閉じないように前面化(定番の小技)
        SetForegroundWindow(ownerHwnd)

        ' 表示して選択されたコマンドIDを取得
        Dim cmd As UInteger = TrackPopupMenuEx(
            hMenu,
            TPM.RETURNCMD Or TPM.RIGHTBUTTON,
            x,
            y,
            ownerHwnd,
            IntPtr.Zero
        )

        If cmd <> 0UI Then
            Dim invoke As New CMINVOKECOMMANDINFOEX()
            invoke.cbSize = Marshal.SizeOf(GetType(CMINVOKECOMMANDINFOEX))
            invoke.fMask = CMIC.ASYNCOK Or CMIC.UNICODE
            invoke.hwnd = ownerHwnd
            invoke.lpVerb = New IntPtr(CInt(cmd - idCmdFirst))
            invoke.nShow = SW_SHOWNORMAL

            Dim ic As Integer = cm.InvokeCommand(invoke)
            If ic < 0 Then Marshal.ThrowExceptionForHR(ic)
        End If

        ' TrackPopupMenu系の挙動安定化(よくある対策)
        PostMessage(ownerHwnd, WM_NULL, IntPtr.Zero, IntPtr.Zero)

    Finally
        ' 表示が終わったら参照を外す(WndProc転送も停止)
        _cm3 = Nothing
        _cm2 = Nothing

        If hMenu <> IntPtr.Zero Then DestroyMenu(hMenu)
        If cm IsNot Nothing Then Marshal.FinalReleaseComObject(cm)
        If parent IsNot Nothing Then Marshal.FinalReleaseComObject(parent)
        If pidlAbs <> IntPtr.Zero Then ILFree(pidlAbs)
        If pUnkContextMenu <> IntPtr.Zero Then Marshal.Release(pUnkContextMenu)
    End Try
End Sub

' --- COM interfaces ---

<ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
 Guid("000214e4-0000-0000-C000-000000000046")>
Private Interface IContextMenu
    <PreserveSig>
    Function QueryContextMenu(hMenu As IntPtr,
                              indexMenu As UInteger,
                              idCmdFirst As UInteger,
                              idCmdLast As UInteger,
                              uFlags As CMF) As Integer

    <PreserveSig>
    Function InvokeCommand(ByRef pici As CMINVOKECOMMANDINFOEX) As Integer

    <PreserveSig>
    Function GetCommandString(idCmd As UInteger,
                              uType As UInteger,
                              pReserved As IntPtr,
                              pszName As IntPtr,
                              cchMax As UInteger) As Integer
End Interface

<ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
 Guid("000214f4-0000-0000-C000-000000000046")>
Private Interface IContextMenu2
    Inherits IContextMenu

    <PreserveSig>
    Function HandleMenuMsg(uMsg As UInteger, wParam As IntPtr, lParam As IntPtr) As Integer
End Interface

<ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
 Guid("BCFCE0A0-EC17-11d0-8D10-00A0C90F2719")>
Private Interface IContextMenu3
    Inherits IContextMenu2

    <PreserveSig>
    Function HandleMenuMsg2(uMsg As UInteger, wParam As IntPtr, lParam As IntPtr, ByRef plResult As IntPtr) As Integer
End Interface

<ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
 Guid("000214E6-0000-0000-C000-000000000046")>
Private Interface IShellFolder
    <PreserveSig>
    Function ParseDisplayName(hwnd As IntPtr, pbc As IntPtr, pszDisplayName As String,
                              ByRef pchEaten As UInteger, ByRef ppidl As IntPtr,
                              ByRef pdwAttributes As UInteger) As Integer

    <PreserveSig>
    Function EnumObjects(hwnd As IntPtr, grfFlags As UInteger, ByRef ppenumIDList As IntPtr) As Integer

    <PreserveSig>
    Function BindToObject(pidl As IntPtr, pbc As IntPtr, ByRef riid As Guid, ByRef ppv As IntPtr) As Integer

    <PreserveSig>
    Function BindToStorage(pidl As IntPtr, pbc As IntPtr, ByRef riid As Guid, ByRef ppv As IntPtr) As Integer

    <PreserveSig>
    Function CompareIDs(lParam As IntPtr, pidl1 As IntPtr, pidl2 As IntPtr) As Integer

    <PreserveSig>
    Function CreateViewObject(hwndOwner As IntPtr, ByRef riid As Guid, ByRef ppv As IntPtr) As Integer

    <PreserveSig>
    Function GetAttributesOf(cidl As UInteger, apidl() As IntPtr, ByRef rgfInOut As UInteger) As Integer

    <PreserveSig>
    Function GetUIObjectOf(hwndOwner As IntPtr, cidl As UInteger, apidl() As IntPtr,
                           ByRef riid As Guid, rgfReserved As IntPtr, ByRef ppv As IntPtr) As Integer

    <PreserveSig>
    Function GetDisplayNameOf(pidl As IntPtr, uFlags As UInteger, ByRef pName As IntPtr) As Integer

    <PreserveSig>
    Function SetNameOf(hwnd As IntPtr, pidl As IntPtr, pszName As String, uFlags As UInteger, ByRef ppidlOut As IntPtr) As Integer
End Interface

' --- Structures / Enums ---

<StructLayout(LayoutKind.Sequential)>
Private Structure CMINVOKECOMMANDINFOEX
    Public cbSize As Integer
    Public fMask As CMIC
    Public hwnd As IntPtr
    Public lpVerb As IntPtr
    Public lpParameters As IntPtr
    Public lpDirectory As IntPtr
    Public nShow As Integer
    Public dwHotKey As Integer
    Public hIcon As IntPtr
    Public lpTitle As IntPtr
    Public lpVerbW As IntPtr
    Public lpParametersW As IntPtr
    Public lpDirectoryW As IntPtr
    Public lpTitleW As IntPtr
    Public ptInvoke As POINT
End Structure

<StructLayout(LayoutKind.Sequential)>
Private Structure POINT
    Public x As Integer
    Public y As Integer
End Structure

<Flags>
Private Enum CMF As UInteger
    NORMAL = 0UI
    EXPLORE = &H4UI
    EXTENDEDVERBS = &H100UI
End Enum

<Flags>
Private Enum TPM As UInteger
    RIGHTBUTTON = &H2UI
    RETURNCMD = &H100UI
End Enum

<Flags>
Private Enum CMIC As UInteger
    UNICODE = &H4000UI
    ASYNCOK = &H100000UI
End Enum

' --- P/Invoke ---

<DllImport("shell32.dll", EntryPoint:="ILCreateFromPathW", CharSet:=CharSet.Unicode, ExactSpelling:=True)>
Private Shared Function ILCreateFromPathW(pszPath As String) As IntPtr
End Function

<DllImport("shell32.dll", ExactSpelling:=True)>
Private Shared Sub ILFree(pidl As IntPtr)
End Sub

<DllImport("shell32.dll", ExactSpelling:=True)>
Private Shared Function SHBindToParent(pidl As IntPtr,
                                      ByRef riid As Guid,
                                      <Out, MarshalAs(UnmanagedType.Interface)> ByRef ppv As IShellFolder,
                                      ByRef ppidlLast As IntPtr) As Integer
End Function

<DllImport("user32.dll", ExactSpelling:=True)>
Private Shared Function CreatePopupMenu() As IntPtr
End Function

<DllImport("user32.dll", ExactSpelling:=True)>
Private Shared Function DestroyMenu(hMenu As IntPtr) As Boolean
End Function

<DllImport("user32.dll", ExactSpelling:=True)>
Private Shared Function TrackPopupMenuEx(hMenu As IntPtr,
                                         uFlags As TPM,
                                         x As Integer,
                                         y As Integer,
                                         hwnd As IntPtr,
                                         lptpm As IntPtr) As UInteger
End Function

<DllImport("user32.dll", ExactSpelling:=True)>
Private Shared Function SetForegroundWindow(hwnd As IntPtr) As Boolean
End Function

<DllImport("user32.dll", ExactSpelling:=True)>
Private Shared Function PostMessage(hWnd As IntPtr, Msg As UInteger, wParam As IntPtr, lParam As IntPtr) As Boolean
End Function


End Class

つまずきポイントと対策(実運用で差が出るところ)

「出たけど一部が出ない」「拡張だけ欠ける」「環境によって動かない」といった相談が多いのは、ほとんどが次の論点に集約されます。

症状よくある原因対策
特定のシェル拡張だけ出ないx86/x64のビット数不一致(拡張が読み込めない)OSに合わせてx64ビルド(またはAny CPU+Prefer 32-bitを見直す)
メニューが崩れる/真っ白/アイコンが欠けるWndProcでWM_DRAWITEM/WM_MEASUREITEM等を転送していないIContextMenu2.HandleMenuMsg(可能ならIContextMenu3)へ転送
メニューが出てもクリック後に何も起きないInvokeCommandのlpVerbがずれている/idCmdFirstとの対応が取れていないTrackPopupMenuの戻り値から必ず(cmd – idCmdFirst)を渡す
メニューが一瞬で閉じる/外側クリックで変な挙動SetForegroundWindow/WM_NULLがない場合に起きやすい表示直前にSetForegroundWindow、表示後にPostMessage(WM_NULL)
UIスレッド以外から呼ぶと不安定COM/メッセージループ前提(STA)を満たしていないWinForms UIスレッドで呼ぶ(別スレッドならSTA+MessageLoopを用意)
アプリが落ちる/固まるサードパーティ拡張が自プロセスに読み込まれ、例外/デッドロックを起こす必要最小限に使う。可能なら別プロセス分離(後述)も検討

Windows 11の右クリック(新デザイン)との関係

Windows 11ではExplorerの右クリックメニューが刷新されましたが、アプリがIContextMenuで表示するメニューは、環境によっては従来型(クラシック)に見えることがあります。重要なのは見た目ではなく、

  • Explorerが提供する標準の動作
  • シェル拡張が提供する動作

を「同じように呼び出せる」点です。見た目まで完全一致させたい要求は難易度が上がるため、業務アプリではまず機能一致(同じ項目と同じ実行結果)をゴールに据えるのが現実的です。

複数ファイル選択に拡張したい場合の考え方

Explorerのように複数選択時のメニューを出したい場合、IShellFolder.GetUIObjectOfに渡すPIDL配列を複数にします。ただしコツがあります。

  • 基本は「同一フォルダ内の複数ファイル」なら相対PIDL配列で対応しやすい
  • 別フォルダを跨ぐ選択を完全再現するには、IDataObject経由などさらに踏み込んだ実装が必要になることがある

まずは、アプリ内の一覧が「同一ディレクトリを表示する設計」なら、複数PIDLを渡す拡張でExplorerに近い体験を作りやすいです。

既製ライブラリを使う場合のチェックリスト

自前実装が重い場合は、シェルコンテキストメニューをラップしたライブラリを使うのも現実的です。ただし「出るけど崩れる」「拡張が欠ける」問題はラッパーでも起こるため、選定では次を確認すると失敗しにくいです。

確認ポイント理由
x64対応が明確か拡張のビット数と合わないと項目が欠けることがある
IContextMenu2/3のメッセージ転送を実装しているかオーナードロー拡張で表示崩れを防ぐため
COM/ハンドル解放が確実かリークや不安定化(メニューが出なくなる等)を防ぐため
例外時の挙動(落ちないか)拡張は自プロセスに読み込まれるため、保護が重要

実運用でおすすめの設計(安定性を上げるコツ)

シェル拡張は便利ですが、あなたのアプリのプロセス内で実行されます。安定性と保守性を考えるなら、次の設計が効きます。

  • 表示の入口を1箇所に集約:メニュー表示処理をクラス化し、フォームごとに散らさない
  • メニュー表示中だけ参照を保持:IContextMenu2/3の参照は使い終わったら必ずNothingへ
  • x64を基本にする:Windowsがx64なら、まずx64で揃える
  • 落ちやすい環境では分離も検討:どうしても拡張が原因で落ちるなら、メニュー表示を別プロセスに逃がす(IPCで結果だけ返す)設計も有効

「右クリックメニューは便利だけど、業務アプリが落ちたら本末転倒」というケースは多いので、使いどころ(ファイル操作が主機能か、補助機能か)も合わせて判断するのがコツです。

まとめ

  • VB.NETでも、Explorerと同等のネイティブ右クリックメニューはIContextMenu/IContextMenu2で実現できる
  • 基本の流れは「PIDL取得 → 親IShellFolder → GetUIObjectOf → QueryContextMenu → TrackPopupMenu → InvokeCommand」
  • 拡張メニューの表示崩れ対策として、WndProcでIContextMenu2(可能なら3)へメッセージ転送が重要
  • x86/x64、STA、リソース解放、拡張の安定性など、実運用の落とし穴を押さえると完成度が上がる

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次