Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 59 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
59
Dung lượng
515,23 KB
Nội dung
Miscellaneous Topics 387 from other arguments. This simpler strategy works well where the DLL needs to maintain a table of global or unique data. Calls to MakeArray would update the table and return an incremented sequence number. Calls to UseArray would be triggered to recalculate something that depended on the values in the table. 9.7 A C++ EXCEL NAME CLASS EXAMPLE, xlName This section describes a class that encapsulates the most common named range handling tasks that an add-in is likely to need to do. In particular it facilitates: • the creation of references to already-defined names; • the discovery of the defined name corresponding to a given range reference; • the reading of values from worksheet names (commands and macro sheet functions only); • the assignment of values to worksheet names (commands only); • the creation and deletion of worksheet names (commands only); • the creation and deletion of DLL-internal names (all DLL functions); • the assignment of an internal name to the calling cell. It would be possible to build much more functionality into a class than is contained in xlName, but the point here is to highlight the benefit of even a simple wrapper to the C API’s name-handling capabilities. A more sophisticated class would, for example, provide some exception handling – a subject deliberately not covered by this book. The definition of the class follows. (Note that the class uses the cpp xloper class for two of its data members.) The definition and code are contained in the example project on the CD ROM in the files XllNames.h and XllNames.cpp respectively. class xlName { public: // // constructors & destructor // xlName():m_Defined(false),m_RefValid(false),m_Worksheet(false){} xlName(const char *name) {Set(name);} // Reference to existing range ∼xlName() {Clear();} // Copy constructor uses operator= function xlName(const xlName & source) {*this = source;} // // Overloaded operators // // Object assignment operator xlName & operator=(const xlName& source); // // Assignment operators place values in cell(s) that range refers to. // Cast operators retrieve values or assign nil if range is not valid // or conversion was not possible. Casting to char * will return // dynamically allocated memory that the caller must free. // 388 Excel Add-in Development in C/C++ int operator=(int); bool operator=(bool b); double operator=(double); WORD operator=(WORD e); const char * operator=(const char *); const xloper * operator=(const xloper *); const xloper12 * operator=(const xloper12 *); const cpp_xloper & operator=(const cpp_xloper &); const VARIANT * operator=(const VARIANT *); const xl4_array * operator=(const xl4_array *array); double operator+=(double); double operator++(void) {return operator+=(1.0);} double operator (void) {return operator+=(-1.0);} operator int(void) const; operator bool(void) const; operator double(void) const; operator char *(void) const; // DLL-allocated copy, caller must free operator wchar_t *(void) const; // DLL-allocated copy, caller must free bool IsDefined(void) const {return m_Defined;} bool IsRefValid(void) const {return m_RefValid;} bool IsWorksheetName(void) const {return m_Worksheet;} char *GetDef(void) const; // get definition as string (caller must free) char *GetName(void) const; // returns deep copy that caller must free void GetName(cpp_xloper &Name) const; // Initialises cpp_xloper to name bool GetRangeSize(DWORD &size) const; bool GetRangeSize(RW &rows, COL &columns) const; bool GetValues(cpp_xloper &Ref) const; // range contents as xltypeMulti bool GetValues(double *array, DWORD array_size) const; bool GetRef(cpp_xloper &Values) const; // range as xltypeRef bool SetValues(const cpp_xloper &Values); bool SetValues(const double *array, RW rows, COL columns); bool NameIs(const char *name) const; bool RefreshRef(void); // refreshes state of name and defn ref // SetToRef sets instance to ref's name if it exists bool SetToRef(const cpp_xloper &Ref, bool internal); bool SetToCallersName(void); // set to caller's name if it exists bool NameCaller(const char *name); // create internal name for caller bool Set(const char *name); // Create a reference to an existing range bool Define(const cpp_xloper &Definition, bool in_dll); bool Define(const char *name, const cpp_xloper &Definition, bool in_dll); void Delete(void); // Delete name and free instance resources void Clear(void); // Clear instance memory but don't delete name void SetNote(const char *text); // Doesn't work - might be C API bug char *GetNote(void); static int GetNumInternalNames(void) {return m_NumInternalNames;} static void IncrNumInternalNames(void) {m_NumInternalNames++;} static void DecrNumInternalNames(void) {m_NumInternalNames ;} private: static int m_NumInternalNames; // Keep a count of all created names protected: bool m_Defined; // Name has been defined bool m_RefValid; // Name's definition (if a ref) is valid bool m_Worksheet; // Name is worksheet name, not internal to DLL cpp_xloper m_RangeRef; cpp_xloper m_RangeName; }; Miscellaneous Topics 389 Note that the overloaded operators (char *) and (wchar_t *) return a deep copy of the contents of the named cell as a null-terminated string which needs to be freed by the caller using free(). One version of GetName() also returns a null-terminated string which needs to be freed explicitly by the caller, whereas the other relies on the cpp_xloper class to manage the memory. A simple example of the use of this class is the function range_name() which returns the defined name corresponding to the given range reference. This function is also included in the example project on the CD ROM and is registered with Excel as RangeName(). Note that the function is registered with the type string "RRP#" (volatile by default) so that the first argument is passed as a reference rather than being de-referenced to a value, as happens with the second argument. xloper * __stdcall range_name(xloper *p_ref, xloper *p_dll) { xlName R; // Are we looking for a worksheet name or a DLL name? bool dll = (p_dll->xltype==xltypeBool && p_dll->val._xbool != 0); if(!R.SetToRef(p_ref, dll)) return p_xlErrRef; cpp_xloper RetVal; R.GetName(RetVal); return RetVal.ExtractXloper(); } The following section provides other examples of the use of this class as well as listings of some of the code. 9.8 KEEPING TRACK OF THE CALLING CELL OF A DLL FUNCTION Consider a worksheet function, call it CreateOne, that creates a data structure within the DLL unique to the cell from which the function is called. There are a number of things that have to be considered: • What happens if the user moves the calling cell and Excel recalculates the function? How will the function know that the thing originally created is still to be associated with the cell in its new position, instead of creating a new one for the new cell location? • What happens if the user clears the formula from the cell? What happens if the user deletes the cell with a column or row deletion or by pasting another cell over it? What happens if the worksheet is deleted or the workbook closed? How will the DLL know how to clean up the resources that the thing was using? If these questions cannot be addressed properly in your DLL, then you will spring memory leaks (at the very least). The same questions arise where a function is sending some request to a remote process or placing a task on a background thread. The answers to these questions all revolve around an ability to keep track of the calling cell that created 390 Excel Add-in Development in C/C++ the internal object, or remote request, or background task. In general, this needs to be done when: • The DLL is maintaining large data structures in the DLL (see above section). • A background thread is used to perform lengthy computations. The DLL needs to know how to return the result to the right cell when next called, bearing in mind the cell may have been moved in the meantime. • The cell is being used as a means of contributing data, that is only allowed to have one source of updates, to a remote application. • The cell is being used to create a request for data from a remote application. Finding out which cell called a worksheet function is done using the C API function xlfCaller. However, given that the user can move/delete/overwrite a cell, the cell reference itself cannot be relied upon to be constant from one call to the next. The solution is to name the calling cell; that is, define a name whose definition is the range reference of the calling cell. For a worksheet function to name the calling cell, the name can only be an internal DLL name created using xlfSetName. (Worksheet names can only be created from commands.) The xlfSetName function is used to define a hidden DLL name. As with regular worksheet names, Excel takes care of altering the definition of the name whenever the corresponding cell is moved. Also, the DLL can very straightforwardly check that the definition is still valid (for example, that the cell has not been deleted in a row or column deletion) and that it still contains the function for which the name was originally created. The class discussed in section section 9.7 A C++ Excel name class example, xlName , on page 387, contains a member function that initialises a class instance to the internal name that corresponds to the calling cell, if it exists, or names it otherwise. Many of the code examples that follow use this class which is provided in the example project on the CD ROM. The sections that immediately follow use the class’ member function code to demonstrate the handling of internal names, etc. 9.8.1 Generating a unique name Generating a valid and unique name for a cell is not too complex and various methods can be devised that will do this. Here’s an example: 1. Get the current time as an integer in the form of seconds from some base time. 2. Increment a counter for the number of names created within this second. (See multi- threading note below). 3. Create a name that incorporates text representations these two numbers. 7 (This could be a simple 0–9 representation or something more compact if storage space and string comparison speed are concerns.) Multi-threading note: The counter used to record how many names have been created in this second needs to be accessible by all threads and so protected by a critical section 7 The name created must conform to the rules described in section 8.11 Working with Excel names on page 316. Miscellaneous Topics 391 where your code might be called from XLL functions that are declared as thread-safe in Excel 2007+. The second example below shows a class that achieves this. The following code shows an example of just such a method that is not thread-safe. Apart from the problems that could arise if multiple threads are trying to access the static variables, two or more threads could return the same not-so-unique name. #include <windows.h> #include <stdio.h> #include <time.h> unsigned long now_serial_seconds(void) { time_t time_t_T; time(&time_t_T); tm tm_T = *localtime(&time_t_T); return (unsigned long)tm_T.tm_sec + 60 * (tm_T.tm_min + 60 * (tm_T.tm_hour + 24 * (tm_T.tm_yday + 366 * tm_T.tm_year % 100))); } // This function is not thread-safe char *make_unique_name(void) { static long name_count = 0; static unsigned long T_last = 0; // Need an unsigned long to contain max possible value unsigned long T = now_serial_seconds(); if(T != T_last) { T_last = T; name_count = 0; } char buffer[32]; // More than enough space // Increment name_count so that names created in the current // second are still unique. The name_count forms the first // part of the name. int ch_count = sprintf(buffer, "x%ld.", ++name_count); int r; // Represent the time number in base 62 using 0-9, A-Z, a-z. // Puts the characters most likely to differ at the front // of the name to optimise name searches and comparisons for(;T; T /= 62) { if((r = T % 62) < 10) r+='0'; else if(r < 36) r+='A' -10; else r+='a' -36; buffer[ch_count++] = r; } buffer[ch_count] = 0; 392 Excel Add-in Development in C/C++ // Make a copy of the string and return it char *new_name = (char *)malloc(ch_count + 1); strcpy(new_name, buffer); return new_name; // caller must free the memory } The following code wraps the generation of unique names in a C++ class that can be called by multiple threads simultaneously and will still generate unique names. class UniqueNameFactory { public: UniqueNameFactory(void) { InitializeCriticalSection(&m_CS); m_Count = 0; m_Tlast = 0; } ∼UniqueNameFactory(void) { DeleteCriticalSection(&m_CS); } char *GetNewName(void) { // Need an unsigned long to contain max possible value unsigned long T = now_serial_seconds(); EnterCriticalSection(&m_CS); if(T != m_Tlast) { m_Tlast = T; m_Count = 0; } else { ++m_Count; } char buffer[32]; // More than enough space // Increment name_count so that names created in the current // second are still unique. The name_count forms the first // part of the name. int ch_count = sprintf(buffer, "x%ld.", m_Count); LeaveCriticalSection(&m_CS); int r; // Represent the time number in base 62 using 0-9, A-Z, a-z. // Puts the characters most likely to differ at the front // of the name to optimise name searches and comparisons for(;T; T /= 62) { if((r = T % 62) < 10) r+='0'; else if(r < 36) r+='A' -10; else r+='a' -36; Miscellaneous Topics 393 buffer[ch_count++] = r; } buffer[ch_count] = 0; // Make a copy of the string and return it char *new_name = (char *)malloc(ch_count + 1); strcpy(new_name, buffer); return new_name; // caller must free the memory } private: long m_Count; unsigned long m_Tlast; CRITICAL_SECTION m_CS; }; 9.8.2 Obtaining the internal name of the calling cell The steps for this are: 1. Get a reference to the calling cell using xlfCaller. 2. Convert the reference to a full address specifier complete with workbook and sheet name in R1C1 form using xlfReftext. 3. Get the name, if it exists, from the R1C1 reference using xlfGetDef. The following two pieces of code list two member functions of the xlName class that, together, perform these steps. bool xlName::SetToCallersName(void) { Clear(); // Get a reference to the calling cell cpp_xloper Caller; if(!Caller.Excel(xlfCaller) != xlretSuccess) return false; return SetToRef(&Caller, true); // true: look for internal name } bool xlName::SetToRef(const cpp_xloper &Ref, bool internal) { Clear(); if(!Ref.IsRef()) return false; // // Convert to text of form [Book1.xls]Sheet1!R1C1 // cpp_xloper RefTextR1C1; if(RefTextR1C1.Excel(xlfReftext, 1, &Ref) != xlretSuccess || RefTextR1C1.IsType(xltypeErr)) return false; 394 Excel Add-in Development in C/C++ // // Get the name, if it exists, otherwise fail. // // First look for an internal name (the default if the 2nd // argument to xlfGetDef is omitted). // if(internal) { if(m_RangeName.Excel(xlfGetDef, 1, &RefTextR1C1) != xlretSuccess || !m_RangeName.IsType(xltypeStr)) return m_Defined = m_RefValid = false; m_Worksheet = false; m_Defined = m_RefValid = true; // If name exists and is internal, add to the list. // add_name_record() has no effect if already there. // Need m_Defined = true before calling this: add_name_record(NULL, *this); } else { // Extract the sheet name and specify this explicitly cpp_xloper SheetName; if(SheetName.Excel(xlSheetNm, 1, &Ref) != xlretSuccess || !SheetName.IsType(xltypeStr)) return m_Defined = m_RefValid = false; // Truncate RefTextR1C1 at the R1C1 part char *p = (char *)RefTextR1C1; // need to free this RefTextR1C1 = strchr(p, '!’) + 1; free(p); // free the deep copy // Truncate SheetName at the sheet name p = (char *)SheetName; SheetName = strchr(p, ']' ) + 1; free(p); // free the deep copy if(m_RangeName.Excel(xlfGetDef, 2, &RefTextR1C1, &SheetName) != xlretSuccess || !m_RangeName.IsType(xltypeStr)) return m_Defined = m_RefValid = false; m_Worksheet = true; m_Defined = m_RefValid = true; } return true; } 9.8.3 Naming the calling cell Where internal names are being used, the task is simply one of obtaining a reference to the calling cell and using the function xlfSetName to define a name whose definition is that reference. However, repeated calls to a na ¨ ıve function that did this would lead to more and more names existing. The first thing to consider is whether the caller already has a name associated with it (see section 9.8.2 above). Sometimes the reason for naming a cell will be to associate it with a particular function, not just a given cell. Therefore, it may be necessary to look at whether the calling function Miscellaneous Topics 395 is the function for which the cell was originally named. If not, the appropriate cleaning up or undoing of the old association should occur where necessary. If the name already exists, and is associated with the calling function, then no action need be taken to rename the cell. The following code lists the member function of xlName that names the calling cell, if not already named. Note that if the name is specified and a name already exists, it deletes the old name before creating the new one. bool xlName::NameCaller(const char *name) { // // Check if given internal name already exists for this caller // if(SetToCallersName() && !m_Worksheet) { // If no name specified, then the existing name is what's required if(!name || !*name) return true; // Check if name is the same as the specified one if(m_RangeName == name) return true; // If not, delete the old name, create a new one. Delete(); } // // If no name provided, create a unique name // if(!name || !*name) { char *p = make_unique_name(); m_RangeName = p; free(p); } else { m_RangeName = name; } m_Worksheet = false; // This will be an internal name // // Get a reference to the calling cell // cpp_xloper Caller; if(Caller.Excel(xlfCaller) != xlretSuccess) return m_Defined = m_RefValid = false; // // Associate the new internal name with the calling cell(s) // cpp_xloper RetVal; if(RetVal.Excel(xlfSetName, 2, &m_RangeName, &Caller) != xlretSuccess) return m_Defined = m_RefValid = false; // 396 Excel Add-in Development in C/C++ // Add the new internal name to the list // m_Defined = m_RefValid = true; add_name_record(NULL, *this); return true; } The function add_name_record() adds this new internal name to a list that enables management of all such names. (See next section for details.) A simple example of how you would use xlName’s ability to do this is the following worksheet function name_me() that assigns an internal name to the calling cell, unless it already has one, and returns the name. (This function has no obvious use other than demonstration.) xloper * __stdcall name_me(int create) { if(called_from_paste_fn_dlg()) return p_xlErrValue; // Set the xlName to refer to the calling cell. xlName Caller; bool name_exists = Caller.SetToCallersName(); if(create) { if(!name_exists) Caller.NameCaller(NULL); // Get the defined name. cpp_xloper Name; Caller.GetName(Name); return Name.ExtractXloper(); } // Not creating, so deleting if(!name_exists) return p_xlFalse; // Delete from Excel's own list of defined names Caller.Delete(); // Delete from DLL's list of internal names. This is a // slightly inefficient method, especially if a large // number of internal names are in the list. A more // specific method of deleting from list could easily // be coded. clean_xll_name_list(); return p_xlTrue; } 9.8.4 Internal XLL name housekeeping The reference associated with an internal XLL name can, for a number of reasons, become invalid or no longer refer to an open workbook. The user may have deleted a row or column containing the original caller, or cut and pasted another cell on top of it. The sheet it was on could have been deleted, or the workbook could have been deleted without ever being saved. [...]... calls not being made will eventually result in Excel complaining about a lack of system resources Excel may have difficulty redrawing the screen, saving files, or may crash completely Memory can be easily abused within VBA despite its lack of pointers For example, overwriting memory allocated by VB in a call to String() will cause heap errors that may crash Excel 4 18 Excel Add -in Development in C/C++ Great... is also asking for trouble Passing xloper/xloper12 types with invalid memory pointers to Excel4 ()/ Excel1 2() will cause a crash Such types are strings (xltypeStr), external range references (xltypeRef), arrays (xltypeMulti) and string elements within arrays Memory Excel has allocated in calls to Excel4 (), Excel4 v(), Excel1 2() or Excel1 2v() should be freed with calls to xlFree Leaks resulting from these... sophisticated example of managing and using background threads 9.10.3 Calling the C API from a DLL-created thread This is not permitted Excel is not expecting such calls which will fail in a way which might destabilise or crash Excel This is, of course, unfortunate It would be nice to be 406 Excel Add -in Development in C/C++ able to access the C API in this way, say, to initiate a recalculation asynchronously... 422 Excel Add -in Development in C/C++ // Excel worksheet cell error codes are passed via VB OLE Variant // arguments in ' ulVal' These are equivalent to the offset below // plus the value defined in "xlcall.h" // This is easier than using the VT_SCODE variant property ' scode' #define VT_XL_ERR_OFFSET 21 481 41008ul VARIANT stdcall ExampleVtFn(double Arg1, VARIANT *pArg2) { VARIANT return_vt; VariantInit(&return_vt);... thread_handle = 0; // Thread is defined using a pointer to this function Thread // executes this function and terminates automatically when this // functions returns The void * pointer is interpreted as a pointer // to long containing the number of milliseconds the thread should // sleep in each loop in this example DWORD WINAPI thread_main(void *vp) { for(;keep_thread_running;) { // Do whatever work the... would do fine In other cases, for example, where you are using a function to contribute some piece of real-time data, it may be imperative that the application informs the recipient within a set time that the source cell has been deleted In this case, it might be sufficient to set up a trap for a recalculation event using the xlcOnRecalc function that calls such 3 98 Excel Add -in Development in C/C++ a function... between the interface and the code, but we won’t From here on in this section, the term model is used to distinguish between, say, the C API and COM, and the term interface is used for the function within the DLL that gets called by Excel, and so has to know about this model, and which in turn calls the core logic When using the C API, the model is the Win32 API, or WINAPI, since the add -in is a Win32 DLL... with such a container The intention is not to propose the best way of coding such things, but simply to lay out a complete approach that can be modified to suit coding preferences and experience 410 Excel Add -in Development in C/C++ enum {TASK_PENDING = 0, TASK_CURRENT = 1, TASK_READY = 2, TASK_UNCLAIMED = 4, TASK_COMPLETE = 8} ; struct task { task(); task(int n_args, const cpp_xloper *InArray); ∼task();... Validate the passed -in function address against the table 3 Call the function WARNING: Exposing any function on a worksheet that takes a double and converts it to an address is capable of crashing Excel if the address is used without safeguards Also, 400 Excel Add -in Development in C/C++ care must be taken to ensure that only functions that have the same prototype are included in the verification table... following examples involve memory abuse of one kind or another If Excel allocated some memory, Excel must free it If the DLL allocated some memory, the DLL must free it Using one to free the other’s memory will cause a heap error Overrunning the bounds of memory that Excel has set aside for modify -in- place arguments to DLL functions is an equally effective method of bringing Excel to its knees Over-running . the calling cell that created 390 Excel Add -in Development in C/C++ the internal object, or remote request, or background task. In general, this needs to be done when: • The DLL is maintaining large. simultaneously reading (in thread_example()) and writing (in thread_main()) to the variable thread_counter. In practice, in a Win32 environment, the reading and writing of a 32-bit integer will not. = true; } return true; } 9 .8. 3 Naming the calling cell Where internal names are being used, the task is simply one of obtaining a reference to the calling cell and using the function xlfSetName