Coverage for graphica/gui/file_association.py: 82.3%

67 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-12 16:38 +0000

1"""Windows で .gra と .graphica を Graphica に結び付ける(利用者の範囲 HKCU\\Software\\Classes だけを触る)。 

2 

3インストーラーを使わず exe だけを置いた人のため、環境設定のボタンから登録・解除する。インストーラーは同じ名前で自分で登録する。 

4ソースや pip から動かしているときは、ダブルクリックで起動する exe が無いので登録しない。 

5""" 

6import os 

7import sys 

8 

9PROG_ID = "Graphica.Project" 

10PROG_ID_DESCRIPTION = "Graphica プロジェクト" 

11ASSOCIATED_EXTENSIONS = (".gra", ".graphica") 

12_CLASSES_ROOT = r"Software\Classes" 

13 

14 

15def _winreg(): 

16 import winreg 

17 return winreg 

18 

19 

20def is_supported(): 

21 """登録できるのは、Windows の exe(PyInstaller で固めたもの)から動かしているときだけ。""" 

22 return sys.platform == "win32" and bool(getattr(sys, "frozen", False)) 

23 

24 

25def open_command(executable=None): 

26 return f'"{executable or sys.executable}" "%1"' 

27 

28 

29def _set_default(winreg, path, value): 

30 with winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, path, 0, winreg.KEY_WRITE) as key: 

31 winreg.SetValueEx(key, "", 0, winreg.REG_SZ, value) 

32 

33 

34def _read_default(winreg, path): 

35 try: 

36 with winreg.OpenKey(winreg.HKEY_CURRENT_USER, path) as key: 

37 return winreg.QueryValueEx(key, "")[0] 

38 except OSError: 

39 return None 

40 

41 

42def _delete_tree(winreg, path): 

43 try: 

44 with winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0, winreg.KEY_ALL_ACCESS) as key: 

45 while True: 

46 try: 

47 child = winreg.EnumKey(key, 0) 

48 except OSError: 

49 break 

50 _delete_tree(winreg, path + "\\" + child) 

51 winreg.DeleteKey(winreg.HKEY_CURRENT_USER, path) 

52 except FileNotFoundError: 

53 pass 

54 

55 

56def _notify_shell(): 

57 """エクスプローラーにアイコンと関連付けを読み直させる。失敗しても登録自体は済んでいる。""" 

58 try: 

59 import ctypes 

60 shcne_assocchanged, shcnf_idlist = 0x08000000, 0x0000 

61 ctypes.windll.shell32.SHChangeNotify(shcne_assocchanged, shcnf_idlist, None, None) 

62 except (AttributeError, OSError): 

63 pass 

64 

65 

66def register(executable=None, winreg=None, notify_shell=True): 

67 winreg = winreg or _winreg() 

68 executable = executable or sys.executable 

69 prog_id_path = _CLASSES_ROOT + "\\" + PROG_ID 

70 _set_default(winreg, prog_id_path, PROG_ID_DESCRIPTION) 

71 _set_default(winreg, prog_id_path + r"\DefaultIcon", f'"{executable}",0') 

72 _set_default(winreg, prog_id_path + r"\shell\open\command", open_command(executable)) 

73 for ext in ASSOCIATED_EXTENSIONS: 

74 _set_default(winreg, _CLASSES_ROOT + "\\" + ext, PROG_ID) 

75 if notify_shell: 75 ↛ 76line 75 didn't jump to line 76 because the condition on line 75 was never true

76 _notify_shell() 

77 

78 

79def unregister(winreg=None, notify_shell=True): 

80 """自分が結び付けた拡張子だけを外す(ほかのソフトに変えられていれば触らない)。""" 

81 winreg = winreg or _winreg() 

82 for ext in ASSOCIATED_EXTENSIONS: 

83 path = _CLASSES_ROOT + "\\" + ext 

84 if _read_default(winreg, path) == PROG_ID: 

85 _delete_tree(winreg, path) 

86 _delete_tree(winreg, _CLASSES_ROOT + "\\" + PROG_ID) 

87 if notify_shell: 87 ↛ 88line 87 didn't jump to line 88 because the condition on line 87 was never true

88 _notify_shell() 

89 

90 

91def is_registered(executable=None, winreg=None): 

92 """このアプリの exe で開くよう登録されているか。""" 

93 winreg = winreg or _winreg() 

94 command = _read_default(winreg, _CLASSES_ROOT + "\\" + PROG_ID + r"\shell\open\command") 

95 if command is None or os.path.normcase(command) != os.path.normcase(open_command(executable)): 

96 return False 

97 return all(_read_default(winreg, _CLASSES_ROOT + "\\" + ext) == PROG_ID for ext in ASSOCIATED_EXTENSIONS)