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?

WindowsのフォルダのアイコンをPythonで変更する

Posted at

やりたいこと

Windowsの指定ディレクトリ配下で、フォルダ名に press(デフォルト、大小無視)を含むフォルダを探し、そのフォルダのアイコンを指定した .ico に変更する。

Pythonプログラム

import os
import sys
import argparse
import shutil
import ctypes
from ctypes import wintypes

# --- Windows attribute helpers ---
FILE_ATTRIBUTE_READONLY = 0x00000001
FILE_ATTRIBUTE_HIDDEN   = 0x00000002
FILE_ATTRIBUTE_SYSTEM   = 0x00000004

GetFileAttributesW = ctypes.windll.kernel32.GetFileAttributesW
GetFileAttributesW.argtypes = [wintypes.LPCWSTR]
GetFileAttributesW.restype  = wintypes.DWORD

SetFileAttributesW = ctypes.windll.kernel32.SetFileAttributesW
SetFileAttributesW.argtypes = [wintypes.LPCWSTR, wintypes.DWORD]
SetFileAttributesW.restype  = wintypes.BOOL

SHChangeNotify = ctypes.windll.shell32.SHChangeNotify
# SHCNE_ASSOCCHANGED = 0x8000000, SHCNF_IDLIST = 0x0000
SHCNE_ASSOCCHANGED = 0x08000000
SHCNF_IDLIST = 0x0000

def get_attrs(path):
    attrs = GetFileAttributesW(path)
    if attrs == 0xFFFFFFFF:
        raise OSError(f"GetFileAttributes failed: {path}")
    return attrs

def add_attrs(path, flags):
    try:
        cur = get_attrs(path)
        if not SetFileAttributesW(path, cur | flags):
            raise OSError(f"SetFileAttributes(+{flags}) failed: {path}")
    except OSError:
        # ファイルがまだ無い場合などは無視(後で再設定)
        pass

def remove_attrs(path, flags):
    try:
        cur = get_attrs(path)
        new = cur & (~flags)
        if not SetFileAttributesW(path, new):
            raise OSError(f"SetFileAttributes(-{flags}) failed: {path}")
    except OSError:
        pass

def ensure_folder_readonly(folder):
    add_attrs(folder, FILE_ATTRIBUTE_READONLY)

def mark_desktop_ini_attrs(ini_path):
    # 既に存在していれば一度属性を外して書き込みやすくし、最後に付与
    remove_attrs(ini_path, FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM)
    add_attrs(ini_path, FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM)

def write_desktop_ini(folder, icon_path, dry_run=False):
    """
    フォルダ内に desktop.ini を作成/上書きし、アイコンを設定する。
    既存の desktop.ini があれば .bak にバックアップしてから上書き。
    """
    ini_path = os.path.join(folder, "desktop.ini")
    content = (
        "[.ShellClassInfo]\n"
        f"IconResource={icon_path},0\n"
    )

    if dry_run:
        return ini_path, False, content

    # 既存バックアップ
    if os.path.exists(ini_path):
        bak = ini_path + ".bak"
        try:
            shutil.copy2(ini_path, bak)
        except Exception:
            pass

    # 書き込み(ASCIIで十分。内容は英数字のみ)
    with open(ini_path, "w", encoding="utf-8", newline="\n") as f:
        f.write(content)

    # 属性付与(隠し + システム)
    mark_desktop_ini_attrs(ini_path)

    # フォルダに ReadOnly を付与(Shell が desktop.ini を尊重)
    ensure_folder_readonly(folder)

    return ini_path, True, content

def clear_icon_customization(folder, dry_run=False):
    """
    フォルダの desktop.ini を削除し、ReadOnly 属性を外す(戻し)。
    """
    ini_path = os.path.join(folder, "desktop.ini")
    changed = False

    if dry_run:
        return ini_path, False

    if os.path.exists(ini_path):
        # 属性を外してから削除
        remove_attrs(ini_path, FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM)
        try:
            os.remove(ini_path)
            changed = True
        except Exception as e:
            print(f"[WARN] remove failed: {ini_path} ({e})")

    # フォルダの ReadOnly を外す(他の用途で必要なら外さない方がよいが、ここでは戻す)
    remove_attrs(folder, FILE_ATTRIBUTE_READONLY)

    return ini_path, changed

def refresh_icons():
    # アイコン関連の変更を Explorer に通知(キャッシュ更新のトリガ)
    try:
        SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, None, None)
    except Exception:
        pass

def name_matches(target_name, substring, case_sensitive=False):
    if case_sensitive:
        return substring in target_name
    return substring.lower() in target_name.lower()

def main():
    parser = argparse.ArgumentParser(
        description="指定ディレクトリ配下でフォルダ名に特定文字列を含むフォルダのアイコンを変更/復元します(Windows 10/11)。"
    )
    parser.add_argument("root", help="検索の起点ディレクトリ")
    parser.add_argument("icon", help="適用する .ico(--revert のときは無視されます)")
    parser.add_argument("--substring", default="press", help='検索文字列(デフォルト: "press"')
    parser.add_argument("--case-sensitive", action="store_true", help="大文字小文字を区別して一致判定")
    parser.add_argument("--revert", action="store_true", help="設定を元に戻す(desktop.ini を削除し ReadOnly を解除)")
    parser.add_argument("--dry-run", action="store_true", help="実際には書き換えず、対象のみ表示")
    args = parser.parse_args()

    root = os.path.abspath(args.root)
    icon = os.path.abspath(args.icon)

    if not os.path.isdir(root):
        print(f"[ERROR] ディレクトリが見つかりません: {root}")
        sys.exit(1)

    if not args.revert:
        if not os.path.isfile(icon) or not icon.lower().endswith(".ico"):
            print(f"[ERROR] .ico ファイルが無効です: {icon}")
            sys.exit(1)

    targets = []
    for cur, dirs, files in os.walk(root):
        # os.walk の dirs をそのまま使うと、cur 自身ではなく直下のフォルダ名が得られる
        for d in dirs:
            folder_path = os.path.join(cur, d)
            if name_matches(d, args.substring, args.case_sensitive):
                targets.append(folder_path)

    print(f"[INFO] 対象フォルダ数: {len(targets)}")
    changed = 0

    for folder in targets:
        try:
            if args.revert:
                ini_path, did = clear_icon_customization(folder, dry_run=args.dry_run)
                action = "REVERT"
            else:
                ini_path, did, content = write_desktop_ini(folder, icon, dry_run=args.dry_run)
                action = "SET"
            print(f"{action}: {folder}")
            if args.dry_run:
                if not args.revert:
                    print(f"  would write: {ini_path}\n  ---\n{content}---")
                else:
                    print(f"  would remove: {ini_path}")
            else:
                if did:
                    changed += 1
        except Exception as e:
            print(f"[WARN] 処理失敗: {folder} ({e})")

    if not args.dry_run:
        refresh_icons()
        print(f"[INFO] 変更されたフォルダ数: {changed}")
        print("[INFO] 反映が遅い場合は、エクスプローラーを再起動するかサインアウト/サインインしてください。")

if __name__ == "__main__":
    main()

やっていることは:

  • 該当フォルダ内に desktop.ini を作成(既存があれば自動バックアップ)
  • IconResource=<アイコンパス>,0 を書き込み
  • フォルダに「読み取り専用」属性(Shell にカスタムを読ませる合図)を付与
  • desktop.ini に「システム」「隠し」属性を付与
  • 変更後に Explorer にアイコン更新を通知(SHChangeNotify)

使い方

# 例: D:\Data 以下で「press」を含むフォルダのアイコンを C:\icons\press.ico に変更
python change_folder_icons.py "D:\Data" "C:\icons\press.ico"

# 大小区別して検索したい場合
python change_folder_icons.py "D:\Data" "C:\icons\press.ico" --case-sensitive

# 検索語を変えたい場合(例: "compress")
python change_folder_icons.py "D:\Data" "C:\icons\press.ico" --substring compress

# まずは実際には書き換えず対象だけ確認
python change_folder_icons.py "D:\Data" "C:\icons\press.ico" --dry-run

# 変更を元に戻す(desktop.ini を削除し、フォルダの ReadOnly 属性を外す)
python change_folder_icons.py "D:\Data" "C:\icons\press.ico" --revert
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?