0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Python] Windowsでディレクトリを開く際に、Explorerのプロセスが残らないようにする

0
Last updated at Posted at 2026-01-20

はじめに

Pythonを使い、WindowsのExplorerでディレクトリを開くには、以下で十分に機能します。

import subprocess

path = r".\Cat"
cmd = ["explorer", path]
subprocess.run(cmd)
print("ok")

しかし、このようにしてExplorerを呼ぶと、呼んだ分だけのExplorerプロセスが残り続けてしまいます。
Popen()で開いてterminate()を実行してもExplorerは終了しません。
そもそもrun()で呼んでいるのにPythonに実行が戻ってきてしまいます。
つまり、Explorerを呼んだプロセス自体は、正常に終了しているのだと推測できます。

import subprocess

path = r".\Cat"
cmd = ["explorer", path]
p = subprocess.Popen(cmd)
p.wait()
p.terminate()
print("ok")

プロセスが残ってしまうのは問題なので、プロセスが残らない方法を調べてみました。
Windows APIでExplorerを呼ぶ関数は、少なくとも2つあります。
使い分けとしては以下になります。

  1. ディレクトリを開くだけで良い場合、ShellExecuteExW()を使う
  2. ディレクトリを開いてファイルを選択状態にする場合、SHOpenFolderAndSelectItems()を使う

環境

Python 3.14.2 (tags/v3.14.2:df79316, Dec  5 2025, 17:18:21) [MSC v.1944 64 bit (AMD64)]

コード

import ctypes as ct
from ctypes import wintypes as wt

Shell32 = ct.windll.Shell32
Ole32 = ct.windll.Ole32

SEE_MASK_DEFAULT = 0x00000000
SEE_MASK_CLASSNAME = 0x00000001
SEE_MASK_CLASSKEY = 0x00000003
SEE_MASK_IDLIST = 0x00000004
SEE_MASK_INVOKEIDLIST = 0x0000000C
SEE_MASK_ICON = 0x00000010
SEE_MASK_HOTKEY = 0x00000020
SEE_MASK_NOCLOSEPROCESS = 0x00000040
SEE_MASK_CONNECTNETDRV = 0x00000080
SEE_MASK_NOASYNC = 0x00000100
SEE_MASK_FLAG_DDEWAIT = 0x00000100
SEE_MASK_DOENVSUBST = 0x00000200
SEE_MASK_FLAG_NO_UI = 0x00000400
SEE_MASK_UNICODE = 0x00004000
SEE_MASK_NO_CONSOLE = 0x00008000
SEE_MASK_ASYNCOK = 0x00100000
SEE_MASK_NOQUERYCLASSSTORE = 0x01000000
SEE_MASK_HMONITOR = 0x00200000
SEE_MASK_NOZONECHECKS = 0x00800000
SEE_MASK_WAITFORINPUTIDLE = 0x02000000
SEE_MASK_FLAG_LOG_USAGE = 0x04000000
SEE_MASK_FLAG_HINST_IS_SITE = 0x08000000

SW_HIDE = 0
SW_SHOWNORMAL = SW_NORMAL = 1
SW_SHOWMINIMIZED = 2
SW_SHOWMAXIMIZED = SW_MAXIMIZE = 3
SW_SHOWNOACTIVATE = 4
SW_SHOW = 5
SW_MINIMIZE = 6
SW_SHOWMINNOACTIVE = 7
SW_SHOWNA = 8
SW_RESTORE = 9
SW_SHOWDEFAULT = 10
SW_FORCEMINIMIZE = 11

SE_ERR_FNF = 2
SE_ERR_PNF = 3
SE_ERR_ACCESSDENIED = 5
SE_ERR_OOM = 8
SE_ERR_DLLNOTFOUND = 32
SE_ERR_SHARE = 26
SE_ERR_ASSOCINCOMPLETE = 27
SE_ERR_DDETIMEOUT = 28
SE_ERR_DDEFAIL = 29
SE_ERR_DDEBUSY = 30
SE_ERR_NOASSOC = 31

# typedef struct _SHELLEXECUTEINFOW {
#   DWORD     cbSize;
#   ULONG     fMask;
#   HWND      hwnd;
#   LPCWSTR   lpVerb;
#   LPCWSTR   lpFile;
#   LPCWSTR   lpParameters;
#   LPCWSTR   lpDirectory;
#   int       nShow;
#   HINSTANCE hInstApp;
#   void      *lpIDList;
#   LPCWSTR   lpClass;
#   HKEY      hkeyClass;
#   DWORD     dwHotKey;
#   union {
#     HANDLE hIcon;
#     HANDLE hMonitor;
#   } DUMMYUNIONNAME;
#   HANDLE    hProcess;
# } SHELLEXECUTEINFOW, *LPSHELLEXECUTEINFOW;

class DUMMYUNIONNAME(ct.Union):
  _fields_ = (
    ("hIcon", wt.HANDLE),
    ("hMonitor", wt.HANDLE),
  )

class SHELLEXECUTEINFOW(ct.Structure):
  _fields_ = (
    ("cbSize", wt.DWORD),
    ("fMask", wt.ULONG),
    ("hwnd", wt.HWND),
    ("lpVerb", wt.LPCWSTR),
    ("lpFile", wt.LPCWSTR),
    ("lpParameters", wt.LPCWSTR),
    ("lpDirectory", wt.LPCWSTR),
    ("nShow", ct.c_int),
    ("hInstApp", wt.HINSTANCE),
    ("lpIDlist", ct.c_void_p),
    ("lpClass", wt.LPCWSTR),
    ("hkeyClass", wt.HKEY),
    ("dwHotkey", wt.DWORD),
    ("dummyUnionName", DUMMYUNIONNAME),
    ("hProcess", wt.HANDLE),
  )

  def __init__(
    self,
    fMask=0,
    hwnd=None,
    lpVerb=None,
    lpFile=None,
    lpParameters=None,
    lpDirectory=None,
    nShow=SW_NORMAL,
    hInstApp=None,
    lpIDlist=None,
    lpClass=None,
    hkeyClass=None,
    dwHotkey=0,
    dummyUnionName=None,
    hProcess=None,
  ):
    super().__init__()
    self.cbSize = ct.sizeof(SHELLEXECUTEINFOW)
    self.fMask = fMask
    self.hwnd = hwnd
    self.lpVerb = lpVerb
    self.lpFile = lpFile
    self.lpParameters = lpParameters
    self.lpDirectory = lpDirectory
    self.nShow = nShow
    self.hInstApp = hInstApp
    self.lpIDlist = lpIDlist
    self.lpClass = lpClass
    self.hkeyClass = hkeyClass
    self.dwHotkey = dwHotkey
    self.dummyUnionName = DUMMYUNIONNAME(dummyUnionName)
    self.hProcess = hProcess

  def __repr__(self):
    return (
      f"{self.cbSize}, {self.fMask}, {self.hwnd}, {self.lpVerb}, {self.lpFile}, {self.lpParameters}, "
      f"{self.lpDirectory}, {self.nShow}, {self.hInstApp}, {self.lpIDlist}, {self.lpClass}, {self.hkeyClass}, "
      f"{self.dwHotkey}, {self.dummyUnionName}, {self.hProcess}"
    )

# typedef struct _SHITEMID {
#   USHORT cb;
#   BYTE   abID[1];
# } SHITEMID;

class SHITEMID(ct.Structure):
  _fields_ = (
    ("cb", wt.USHORT),
    ("abID", wt.BYTE * 1),  # 長さ1の配列
  )

# typedef struct _ITEMIDLIST {
#   SHITEMID mkid;
# } ITEMIDLIST;

class ITEMIDLIST(ct.Structure):
  _fields_ = (("mkid", SHITEMID),)

COINIT_APARTMENTTHREADED = 0x2
COINIT_MULTITHREADED = 0x0
COINIT_DISABLE_OLE1DDE = 0x4
COINIT_SPEED_OVER_MEMORY = 0x8

# typedef LONG HRESULT;

HRESULT = wt.LONG

# BOOL ShellExecuteExW(
#   [in, out] SHELLEXECUTEINFOW *pExecInfo
# );

def callShellExecuteExW(lpFile, lpDirectory=None, lpVerb="open"):
  hr = CoInitializeEx(None, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)
  if hr not in (0, 1):
    msg = f"CoInitialize failed: HRESULT 0x{hr & 0xFFFFFFFF:08X}"
    raise OSError(msg)

  ShExecInfo = SHELLEXECUTEINFOW(lpVerb=lpVerb, lpFile=lpFile, lpDirectory=lpDirectory)
  res = ShellExecuteExW(ShExecInfo)
  CoUninitialize()
  if not res:
    raise ct.WinError()
  return ShExecInfo.hInstApp > SE_ERR_DLLNOTFOUND

# SHSTDAPI SHOpenFolderAndSelectItems(
#   [in]           PCIDLIST_ABSOLUTE     pidlFolder,
#                  UINT                  cidl,
#   [in, optional] PCUITEMID_CHILD_ARRAY apidl,
#                  DWORD                 dwFlags
# );

SHSTDAPI = HRESULT

SHOpenFolderAndSelectItems = Shell32.SHOpenFolderAndSelectItems
SHOpenFolderAndSelectItems.restype = SHSTDAPI
SHOpenFolderAndSelectItems.argtypes = (
  ct.POINTER(ITEMIDLIST),
  wt.UINT,
  ct.POINTER(ct.POINTER(ITEMIDLIST)),
  wt.DWORD,
)

OFASI_EDIT = 0x0001
OFASI_OPENDESKTOP = 0x0002

# HRESULT

S_OK = 0x00000000  # Operation successful
E_NOTIMPL = 0x80004001  # Not implemented
E_NOINTERFACE = 0x80004002  # No such interface supported
E_POINTER = 0x80004003  # Pointer that is not valid
E_ABORT = 0x80004004  # Operation aborted
E_FAIL = 0x80004005  # Unspecified failure
E_UNEXPECTED = 0x8000FFFF  # Unexpected failure
E_ACCESSDENIED = 0x80070005  # General access denied error
E_HANDLE = 0x80070006  # Handle that is not valid
E_OUTOFMEMORY = 0x8007000E  # Failed to allocate necessary memory
E_INVALIDARG = 0x80070057  # One or more arguments are not valid

# #ifdef UNICODE
#  typedef LPCWSTR PCTSTR;
# #else
#  typedef LPCSTR PCTSTR;
# #endif

# PIDLIST_ABSOLUTE ILCreateFromPath(
#   [in] PCTSTR pszPath
# );

ILCreateFromPath = Shell32.ILCreateFromPath
ILCreateFromPath.restype = ct.POINTER(ITEMIDLIST)
ILCreateFromPath.argtypes = (wt.LPCWSTR,)

# void ILFree(
#   [in] PIDLIST_RELATIVE pidl
# );

ILFree = Shell32.ILFree
ILFree.restype = None
ILFree.argtypes = (ct.POINTER(ITEMIDLIST),)

# void CoTaskMemFree(
#   [in, optional] _Frees_ptr_opt_ LPVOID pv
# );

CoTaskMemFree = Ole32.CoTaskMemFree
CoTaskMemFree.restype = None
CoTaskMemFree.argtypes = (wt.LPVOID,)

# SHSTDAPI SHParseDisplayName(
#   [in]            PCWSTR           pszName,
#   [in, optional]  IBindCtx         *pbc,
#   [out]           PIDLIST_ABSOLUTE *ppidl,
#   [in]            SFGAOF           sfgaoIn,
#   [out, optional] SFGAOF           *psfgaoOut
# );

SFGAOF = wt.ULONG

SFGAO_CANCOPY = 0x00000001
SFGAO_CANMOVE = 0x00000002
SFGAO_CANLINK = 0x00000004
SFGAO_STORAGE = 0x00000008
SFGAO_CANRENAME = 0x00000010
SFGAO_CANDELETE = 0x00000020
SFGAO_HASPROPSHEET = 0x00000040
SFGAO_DROPTARGET = 0x00000100
SFGAO_CAPABILITYMASK = 0x00000177
SFGAO_SYSTEM = 0x00001000
SFGAO_ENCRYPTED = 0x00002000
SFGAO_ISSLOW = 0x00004000
SFGAO_GHOSTED = 0x00008000
SFGAO_LINK = 0x00010000
SFGAO_SHARE = 0x00020000
SFGAO_READONLY = 0x00040000
SFGAO_HIDDEN = 0x00080000
SFGAO_DISPLAYATTRMASK = 0x000FC000  # Do not use.
SFGAO_NONENUMERATED = 0x00100000
SFGAO_NEWCONTENT = 0x00200000
# SFGAO_CANMONIKER # Not supported.
# SFGAO_HASSTORAGE # Not supported.
SFGAO_STREAM = 0x00400000
SFGAO_STORAGEANCESTOR = 0x00800000
SFGAO_VALIDATE = 0x01000000
SFGAO_REMOVABLE = 0x02000000
SFGAO_COMPRESSED = 0x04000000
SFGAO_BROWSABLE = 0x08000000
SFGAO_FILESYSANCESTOR = 0x10000000
SFGAO_FOLDER = 0x20000000
SFGAO_FILESYSTEM = 0x40000000
SFGAO_STORAGECAPMASK = 0x70C50008
SFGAO_HASSUBFOLDER = 0x80000000
SFGAO_CONTENTSMASK = 0x80000000
SFGAO_PKEYSFGAOMASK = 0x81044000

SHParseDisplayName = Shell32.SHParseDisplayName
SHParseDisplayName.restype = SHSTDAPI
SHParseDisplayName.argtypes = (
  wt.LPCWSTR,
  ct.c_void_p,  # IBindCtx,
  ct.POINTER(ct.POINTER(ITEMIDLIST)),
  SFGAOF,
  ct.POINTER(SFGAOF),
)

# void CoUninitialize();

CoUninitialize = Ole32.CoUninitialize
CoUninitialize.restype = None
CoUninitialize.argtypes = ()


def getPidList(path):
  path = pathlib.Path(path) if isinstance(path, str) else path
  if not path.exists():
    msg = f"{path} not Found"
    raise FileNotFoundError(msg)

  pidl = ct.pointer(ITEMIDLIST())
  sfgao = SFGAOF(0)
  hr = SHParseDisplayName(str(path), None, ct.byref(pidl), 0, ct.byref(sfgao))
  if hr != S_OK:
    msg = f"SHParseDisplayName failed for {path!r}: HRESULT 0x{hr & 0xFFFFFFFF:08X}"
    raise OSError(msg)
  return pidl


def callSHOpenFolderAndSelectItems(pathDir, pathFiles):
  hr = CoInitializeEx(None, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)
  if hr not in (0, 1):
    msg = f"CoInitialize failed: HRESULT 0x{hr & 0xFFFFFFFF:08X}"
    raise OSError(msg)

  dwFlags = 0
  if pathDir is None:
    pidlDir = None
    dwFlags = OFASI_OPENDESKTOP
  else:
    pidlDir = getPidList(pathDir)
  try:
    pidlFiles = [getPidList(file) for file in pathFiles]
  except (OSError, FileNotFoundError):
    CoTaskMemFree(pidlDir)
    CoUninitialize()
    raise

  count = len(pidlFiles)
  Array_ITEMIDLIST = ct.POINTER(ITEMIDLIST) * count
  arr = Array_ITEMIDLIST(*pidlFiles)

  hr = SHOpenFolderAndSelectItems(pidlDir, count, arr, dwFlags)
  if hr != S_OK:
    msg = f"SHOpenFolderAndSelectItems failed: HRESULT 0x{hr & 0xFFFFFFFF:08X}"
    raise OSError(msg)

  CoTaskMemFree(pidlDir)
  for pidl in pidlFiles:
    CoTaskMemFree(pidl)
  CoUninitialize()

解説

コードには使っていませんが、関連する定数定義も記述したので非常に長くなりました。
そのため、要点だけを解説します。

ShellExecuteExW

def callShellExecuteExW(lpFile, lpDirectory=None, lpVerb="open"):
  hr = CoInitializeEx(None, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)
  if hr not in (0, 1):
    msg = f"CoInitialize failed: HRESULT 0x{hr & 0xFFFFFFFF:08X}"
    raise OSError(msg)

  ShExecInfo = SHELLEXECUTEINFOW(lpVerb=lpVerb, lpFile=lpFile, lpDirectory=lpDirectory)
  res = ShellExecuteExW(ShExecInfo)
  CoUninitialize()
  if not res:
    raise ct.WinError()
  return ShExecInfo.hInstApp > SE_ERR_DLLNOTFOUND

ShellExecuteExWを呼ぶには、コンポーネント オブジェクト モデル(COM)の初期化が必要です。
それを、CoInitializeEx()で行います。

次に、ディレクトリを開くために、SHELLEXECUTEINFOW構造体に値を設定します。
正直なところ良く分からないパラメータも多いですが、今回は、ディレクトリ(ファイル)を開く事だけを考えます。
まず、lpVerbには、"open"を指定します。
lpFileには、開きたいディレクトリ(ファイル)パスを指定します。
作業ディレクトリを明示的に指定したい場合には、lpDirectoryを指定します。

設定したSHELLEXECUTEINFOW構造体を渡してShellExecuteExW()を呼べばディレクトリ(ファイル)が開きます。

CoInitializeEx()を呼んだら、CoUninitialize()で後始末をする必要があるので呼びます。

ShellExecuteExWの使い方

ディレクトリを開く場合は、以下のようにします。

ShellExecuteExWでディレクトリを開く
path = r".\someDirectory"
callShellExecuteExW(path)
# callShellExecuteExW(path, path)
# callShellExecuteExW(None, path)

内部的な違いが分かりませんが、

  • lpFile=Path, lpDirecoty=None
  • lpFile=None, lpDirecoty=Path
  • lpFile=Path, lpDirecoty=Path

は全て同じ動作になります。

記事の目的とは異なりますが、以下のようにlpFileにファイルへのパスを指定すると、拡張子に対応した既定のアプリがファイルを開きます。

ShellExecuteExWでファイルを開く
path = r".\someDirectory\someFile.txt"
callShellExecuteExW(path)

SHOpenFolderAndSelectItems

def getPidList(path):
  path = pathlib.Path(path) if isinstance(path, str) else path
  if not path.exists():
    msg = f"{path} not Found"
    raise FileNotFoundError(msg)

  pidl = ct.pointer(ITEMIDLIST())
  sfgao = SFGAOF(0)
  hr = SHParseDisplayName(str(path), None, ct.byref(pidl), 0, ct.byref(sfgao))
  if hr != S_OK:
    msg = f"SHParseDisplayName failed for {path!r}: HRESULT 0x{hr & 0xFFFFFFFF:08X}"
    raise OSError(msg)
  return pidl


def callSHOpenFolderAndSelectItems(pathDir, pathFiles):
  hr = CoInitializeEx(None, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)
  if hr not in (0, 1):
    msg = f"CoInitialize failed: HRESULT 0x{hr & 0xFFFFFFFF:08X}"
    raise OSError(msg)

  dwFlags = 0
  if pathDir is None:
    pidlDir = None
    dwFlags = OFASI_OPENDESKTOP
  else:
    pidlDir = getPidList(pathDir)
  try:
    pidlFiles = [getPidList(file) for file in pathFiles]
  except (OSError, FileNotFoundError):
    CoTaskMemFree(pidlDir)
    CoUninitialize()
    raise

  count = len(pidlFiles)
  Array_ITEMIDLIST = ct.POINTER(ITEMIDLIST) * count
  arr = Array_ITEMIDLIST(*pidlFiles)

  hr = SHOpenFolderAndSelectItems(pidlDir, count, arr, dwFlags)
  if hr != S_OK:
    msg = f"SHOpenFolderAndSelectItems failed: HRESULT 0x{hr & 0xFFFFFFFF:08X}"
    raise OSError(msg)

  CoTaskMemFree(pidlDir)
  for pidl in pidlFiles:
    CoTaskMemFree(pidl)
  CoUninitialize()

SHOpenFolderAndSelectItems()を呼ぶにも、COMの初期化が必要なのでCoInitializeEx()を実行します。

ディレクトリを開くには、そのディレクトリを表すITEMIDLISTが必要です。
これは、ILCreateFromPath()SHParseDisplayName()で取得できます。
ILCreateFromPath()の方が単純で分かりやすいのですが、Microsoftは、SHParseDisplayName()を使用するように推奨しているようです。
SHILCreateFromPath()を使わずにSHParseDisplayName()を使えとの記述は見つけましたが、ILCreateFromPath()が非推奨という記述は見つけられませんでした。
いずれにせよ、SHParseDisplayName()の方が対応範囲が広いのでこちらを使って損はありません。

SHOpenFolderAndSelectItemsの引数のpidlFolderには、開きたいディレクトリのITEMIDLISTのポインターを渡します。
apidlには、そのディレクトリに存在するファイルのITEMIDLISTの配列のポインターを渡します。

ITEMIDLISTは、使い終わったら解放しなければいけません。
それには、CoTaskMemFree()を使います。
古いWindowsでは、ILFree()を使っていたようですが、現状は使わないようです。

最後に、CoInitializeEx()の後始末として、CoUninitialize()を実行します。

SHOpenFolderAndSelectItemsの注意点

このコードを実行するとエラーも出ずにディレクトリが開くのですが、稀に、最小化・最大化・閉じるボタンが表示されない事があります。
ネットで検索すると出てくる他のコードも同じように表示されない事があるので自分のコードに起因するバグとも言い切れません。
これの対処法が分かれば良いのですが。

SHOpenFolderAndSelectItemsの使い方

以下のようにすると、someFile.txtを選択状態にしてsomeDirectoryが開きます。
また、複数のファイルを渡すと、それらすべてが選択状態で開きます。

dir = r".\someDirectory"
files = [r".\someDirectory\someFile.txt"]
callSHOpenFolderAndSelectItems(dir, files)

以下のようにすると、someDirectoryを選択状態にしてsomeDirectoryの親ディレクトリが開きます。

dir = r".\someDirectory"
files = []
callSHOpenFolderAndSelectItems(dir, files)

最後に、これも記事の目的とは違うのですが、pathにデスクトップへのパスを指定して、filesにデスクトップ上のファイルへのパスを指定すると、デスクトップのファイルが選択状態になります。

path = r"...\デスクトップ"
files = [r"...\デスクトップ\someFile.txt"]
callSHOpenFolderAndSelectItems(path, files)

pathをNoneにしても同じように、デスクトップのファイルが選択状態になります。

files = [r"...\デスクトップ\someFile.txt"]
callSHOpenFolderAndSelectItems(None, files)

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?