1
+ import os
2
+ import logging
3
+ import xml .etree .ElementTree as ET
4
+ import win32com .client
5
+ from .xlam .ui import UI
6
+ from .xlam .vba import VBA
7
+ from .share import (AddInException , copytree )
8
+
9
+
10
+ # logging
11
+ logging .basicConfig (
12
+ level = logging .INFO ,
13
+ format = "[%(levelname)s] %(message)s" )
14
+
15
+
16
+ # configuration path
17
+ SCRIPT_PATH = os .path .dirname (os .path .abspath (__file__ ))
18
+ RESOURCE_PATH = os .path .join (SCRIPT_PATH , 'resources' )
19
+
20
+ RESOURCE_ADDIN = 'xlam'
21
+ RESOURCE_PYTHON = 'python'
22
+ RESOURCE_VBA = 'vba'
23
+
24
+ VBA_GENERAL = 'general'
25
+ VBA_MENU = 'menu'
26
+ VBA_WORKBOOK = 'ThisWorkbook'
27
+
28
+ CUSTOM_UI = 'CustomUI.xml'
29
+
30
+
31
+ class Addin :
32
+
33
+ def __init__ (self , xlam_file :str , visible :bool = False ) -> None :
34
+ '''The Excel add-in object, including ribbon UI and VBA modules.
35
+
36
+ Args:
37
+ xlam_file (str): Add-in file path.
38
+ visible (bool): Process the add-in with Excel application running in the background if False.
39
+ '''
40
+ # work path
41
+ self .xlam_file = xlam_file
42
+ self .path = os .path .dirname (xlam_file )
43
+
44
+ # Add-in VBA modules
45
+ self .excel_app = win32com .client .Dispatch ('Excel.Application' ) # win32 COM object
46
+ self .excel_app .Visible = visible
47
+ self .excel_app .DisplayAlerts = False
48
+
49
+
50
+ def close (self ):
51
+ '''Close add-in and exit Excel.'''
52
+ self .excel_app .Application .Quit ()
53
+
54
+
55
+ def create (self , vba_only :bool = False ):
56
+ '''Create addin file.
57
+ - customize ribbon tab and associated VBA callback according to ui file
58
+ - include VBA modules, e.g., general VBA subroutines for data transferring.
59
+
60
+ Args:
61
+ vba_only (bool, optional): Whether simple VBA addin (without Python related modules).
62
+ Defaults to False.
63
+ '''
64
+ N = 2 if vba_only else 3
65
+
66
+ # create addin file
67
+ logging .info ('(1/%d) Creating add-in structure...' , N )
68
+ ui = UI (self .xlam_file )
69
+ template = os .path .join (RESOURCE_PATH , RESOURCE_ADDIN )
70
+ custom_ui = os .path .join (self .path , CUSTOM_UI )
71
+ ui .create (template , custom_ui )
72
+
73
+ if not os .path .exists (self .xlam_file ):
74
+ raise AddInException ('Create add-in structures failed.' )
75
+
76
+ # update VBA module
77
+ vba = VBA (xlam_file = self .xlam_file , excel_app = self .excel_app )
78
+ base_menu = os .path .join (RESOURCE_PATH , RESOURCE_VBA , f'{ VBA_MENU } .bas' )
79
+
80
+ # 1. import menu module
81
+ logging .info ('(2/%d) Creating menu callback subroutines...' , N )
82
+
83
+ # create callback function module for customized menu button
84
+ callbacks = self .__get_callbacks_from_custom_ui ()
85
+ vba .add_callbacks (VBA_MENU , callbacks , base_menu )
86
+
87
+ # extra steps for VBA-Python combined addin
88
+ if not vba_only :
89
+ logging .info ('(3/%d) Creating Python-VBA interaction modules...' , N )
90
+
91
+ # 2. import workbook module
92
+ workbook_module = os .path .join (RESOURCE_PATH , RESOURCE_VBA , f'{ VBA_WORKBOOK } .cls' )
93
+ vba .import_named_module (VBA_WORKBOOK , workbook_module )
94
+
95
+ # 3. import general module
96
+ general_module = os .path .join (RESOURCE_PATH , RESOURCE_VBA , f'{ VBA_GENERAL } .bas' )
97
+ vba .import_module (general_module )
98
+
99
+ # 4. copy main python scripts
100
+ python_module = os .path .join (RESOURCE_PATH , RESOURCE_PYTHON )
101
+ copytree (python_module , self .path )
102
+
103
+ # save vba modules
104
+ vba .save ()
105
+
106
+
107
+ def update (self ):
108
+ '''Update Ribbon and associated callback functions.
109
+
110
+ NOTE: only update newly added callbacks. Editing existing callback names will be ignored.
111
+ '''
112
+ # update addin with customized ui file
113
+ logging .info ('(1/2) Updating ribbon structures...' )
114
+ custom_ui = os .path .join (self .path , CUSTOM_UI )
115
+ ui = UI (self .xlam_file )
116
+ ui .update (custom_ui )
117
+
118
+ # update VBA menu module
119
+ logging .info ('(2/2) Updating menu callback subroutines...' )
120
+ vba = VBA (xlam_file = self .xlam_file , excel_app = self .excel_app )
121
+ callbacks = self .__get_callbacks_from_custom_ui () # get new callback functions
122
+ vba .update_callbacks (VBA_MENU , callbacks )
123
+
124
+ # save vba modules
125
+ vba .save ()
126
+
127
+
128
+ def __get_callbacks_from_custom_ui (self ) -> list :
129
+ '''parse CustomUI.xml to collect all callback function names, e.g.,
130
+ - attribute=onAction for button, toggleButton and checkBox.
131
+ - attribute=onChange for editBox and comboBox.
132
+ '''
133
+ ui_file = os .path .join (self .path , CUSTOM_UI )
134
+ if not os .path .exists (ui_file ):
135
+ raise AddInException (f'Can not find ribbon file: { CUSTOM_UI } .' )
136
+
137
+ try :
138
+ tree = ET .parse (ui_file )
139
+ except ET .ParseError as e :
140
+ raise AddInException (f'Error format in { CUSTOM_UI } : { str (e )} ' )
141
+ else :
142
+ root = tree .getroot ()
143
+
144
+ # get root and check all nodes by iteration
145
+ callbacks = []
146
+ for attr_name in ('onAction' , 'onChange' ):
147
+ callbacks .extend ([node .attrib .get (attr_name ) \
148
+ for node in root .iter () if attr_name in node .attrib ])
149
+
150
+ if not callbacks :
151
+ raise AddInException (f'No actions defined: { CUSTOM_UI } ' )
152
+
153
+ return callbacks
0 commit comments