├── .gitattributes ├── .gitignore ├── README.md ├── Screenshots ├── ExcelSelection.png ├── RefEditUI.png ├── RefEditUI2.png ├── Ribbon.png └── WholeForm.png └── Source ├── DotNetRefEdit.sln ├── DotNetRefEdit ├── DotNetRefEdit.csproj ├── DotNetRefEdit.dna ├── DotNetRefEdit.xll ├── DotNetRefEdit64.dna ├── DotNetRefEdit64.xll ├── ExcelHelper.cs ├── ExcelSelectionTracker.cs ├── Properties │ └── AssemblyInfo.cs ├── RefEditForm.Designer.cs ├── RefEditForm.cs ├── RefEditForm.resx ├── RefEditWindow.xaml ├── RefEditWindow.xaml.cs ├── Ribbon.cs └── WindowsInterop.cs └── ExcelDna.Integration.dll /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.suo 2 | Source/DotNetRefEdit/bin 3 | Source/DotNetRefEdit/obj 4 | Source/DotNetRefEdit/DotNetRefEdit.csproj.user -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DotNetRefEdit 2 | Examples of RefEdit like controls for Excel add-ins using C# and ExcelDna 3 | 4 | Overview 5 | -------- 6 | This project is a proof of concept. This is not a library to be reused in other solutions. 7 | 8 | The purpose is to show how to build a .Net UI using WinForm or WPF, within an Excel add-in, where the user can select a range in Excel in order to see the range address appear in the UI control. 9 | 10 | Illustration 11 | -------- 12 | 13 | - Step 1: open a form and focus to a "RefEdit" control 14 | 15 | ![Prepare Selection](https://raw.github.com/Ron-Ldn/DotNetRefEdit/master/Screenshots/RefEditUI.png) 16 | 17 | - Step 2: select a range into Excel 18 | 19 | ![Select Range](https://raw.github.com/Ron-Ldn/DotNetRefEdit/master/Screenshots/ExcelSelection.png) 20 | 21 | - Result: the range address is populated automatically into the "RefEdit" control into the form 22 | 23 | ![Populate Range Address](https://raw.github.com/Ron-Ldn/DotNetRefEdit/master/Screenshots/RefEditUI2.png) 24 | 25 | Inventory 26 | -------- 27 | Several projects can be found on the internet which propose to implement the equivalent of the RefEdit control for .Net programs. But as far as I know, none of these projects show how to manage the window itself. Here is a list of issues I faced in the past: 28 | - If the UI runs in the Excel thread, then it will freeze when Excel is busy. 29 | - If the UI runs in the Excel thread, then in some conditions it may be impossible for the user to manually edit a control within the UI because Excel will activate and put the focus to the last selected cell. The conditions to reproduce this are quite unclear to me though. 30 | - If the UI runs in its own thread, then the user will need to click twice into Excel in order to select a range. Actually, the first click will activate the window and then Excel will discard the message. The second click is to select the range into the activated window. 31 | 32 | Links: 33 | - http://blogs.msdn.com/b/gabhan_berry/archive/2008/06/12/net-refedit-control.aspx 34 | - http://www.codeproject.com/Articles/32805/RefEdit-Emulation-for-NET 35 | - http://www.codeproject.com/Articles/34425/VS-NET-Excel-Addin-Refedit-Control 36 | 37 | Solution 38 | -------- 39 | The best option, according to me, is to run the UI in its own thread. The "must-click-twice" issue described above can be resolved by hooking the WH_CALLWNDPROC messages: if the message is of type "WM_MOUSEACTIVATE" and if the handle is an Excel workbook window (in which case, the window name will be "EXCEL7") then it is possible to set the focus to that window before Excel processes the message. 40 | 41 | 1. Hook 42 | ```C# 43 | _hHookCwp = SetWindowsHookEx(HookType.WH_CALLWNDPROC, _procCwp, (IntPtr)0, excelThreadId); 44 | ``` 45 | 46 | 2. Check the class name 47 | ```C# 48 | GetClassNameW(cwpStruct.hwnd, cname, cname.Capacity); 49 | if (cname.ToString() == "EXCEL7") 50 | ``` 51 | 52 | 3. Set the focus 53 | ```C# 54 | SetFocus(cwpStruct.hwnd); 55 | ``` 56 | 57 | Bonus: using the same hook, it is possible to notify the UI when the user clicks on the same cell as before. In fact, the UI is notified of a new selection by the Excel event "SheetSelectionChange", but this event is not triggered if the user points to the same cell again. By adding a special notification to the UI inside the hook method, it is possible to resolve that issue. 58 | 59 | Code 60 | -------- 61 | The solution available along with this project proposes 4 examples, all accessible from the Excel ribbon. These examples have been tested with Excel 2010 32bit, Excel 2013 32bit and Excel 2013 64bit, running on Windows 7 64bit. 62 | 63 | ![Ribbon](https://raw.github.com/Ron-Ldn/DotNetRefEdit/master/Screenshots/Ribbon.png) 64 | 65 | The "Excel Thread" buttons launch a WinForm and a WPF window running in the Excel main thread. The "Separate Thread" buttons launch the same UIs in their own threads. 66 | 67 | When the UI is launched, it will subscribe to the "SheetSelectionChange" event and hook the WH_CALLWNDPROC messages. For more details on how to hook Windows messages, please refer to https://msdn.microsoft.com/en-us/library/windows/desktop/ms644959%28v=vs.85%29.aspx 68 | 69 | The hook method "CwpProc" will apply a specific treatment to WM_MOUSEACTIVATE messages. 70 | 71 | 1. Verify the window name. If the name is not "EXCEL7" then skip the treatment and call next hook. 72 | 73 | https://msdn.microsoft.com/en-us/library/windows/desktop/ms633582%28v=vs.85%29.aspx 74 | 75 | 2. Set the focus to the window using SetFocus. 76 | 77 | https://msdn.microsoft.com/en-us/library/windows/desktop/ms646312%28v=vs.85%29.aspx 78 | 79 | 3. Notify the UI so it can populate the current selection's address into the "RefEdit" control. 80 | 81 | I also included some features like the "F4" shortcut to convert the address, but this is not the main purpose of this project so I will not run into the details. 82 | 83 | Demonstration 84 | -------- 85 | 86 | Note: DotNetRefEdit.xll is for Excel 32bit, DotNetRefEdit64.xll is for Excel 64bit. 87 | 88 | 1. Open the add-in and set "12" in A1 inside the active worksheet. Set "1", "2", 3" and "4" respectively in B1, B2, C1 and C2. 89 | 90 | 2. Click on button "WinForm" in the "Separate Thread" section. Then click on the first text box, next to "Augend". 91 | 92 | ![Prepare Selection](https://raw.github.com/Ron-Ldn/DotNetRefEdit/master/Screenshots/RefEditUI.png) 93 | 94 | 3. Select A1 in the worksheet. **This is possible in one click.** Finally "[Book1]Sheet1!A1" shall appear in the text box. 95 | 96 | 4. Now focus on the second text box, next to "Addend". Then select "B1:C2" in the worksheet. Once again **this is possible in one click**. Finally, "[Book1]Sheet1!B1:C2" will appear in the text box. 97 | 98 | 5. The form will evaluate the sum automatically and "22" will appear in the box next to "Evaluation". 99 | 100 | 6. Now select the output, let's say "A5", in the destination box. Finally click on the "Insert" button and this will insert the formula into A5: "=SUM(Sheet1!A1,Sheet1!B1:C2 )" 101 | 102 | ![WholeForm](https://raw.github.com/Ron-Ldn/DotNetRefEdit/master/Screenshots/WholeForm.png) 103 | 104 | 7. Focus to the first box again, remove the address. Now go to the worksheet, edit A1 and copy the value. Without escaping from the cell edition, return to the UI and paste the text into the text box. It works because the UI runs in its own thread so **it is not frozen when Excel is busy**. 105 | 106 | 8. Focus to the "Augend" box and select A1. This will populate "[Book1]Sheet1!A1" into that box. 107 | 108 | 9. Now focus on the "Addend" box and select A1 again. **The address will appear into the box, despite the "SheetSelectionChange" event was not raised.** 109 | 110 | Conclusion 111 | -------- 112 | 113 | First, I want to thank Govert from the ExcelDna project, for all the help he provided in order to resolve these issues. Big thanks also for the ExcelDna project itself, which is amazing. 114 | 115 | https://github.com/Excel-DNA/ExcelDna 116 | 117 | The **DotNetRefEdit** project demonstrates that it is possible to create .Net add-ins for Excel where UIs can behave like function wizards and allow range selections into Excel, in an user-friendly way. 118 | 119 | The "RefEdit" control itself is not hard to implement. The main difficulty resides in the window management. The solution is quite simple once you know it: hook the WH_CALLWNDPROC messages using the SetWindowsHookEx function ; when the hooked message is a WM_MOUSEACTIVATE and the underlying window is a workbook, then call SetFocus. 120 | -------------------------------------------------------------------------------- /Screenshots/ExcelSelection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ron-Ldn/DotNetRefEdit/1d8d58ec830cf93683467872e93540fdd5ee1f47/Screenshots/ExcelSelection.png -------------------------------------------------------------------------------- /Screenshots/RefEditUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ron-Ldn/DotNetRefEdit/1d8d58ec830cf93683467872e93540fdd5ee1f47/Screenshots/RefEditUI.png -------------------------------------------------------------------------------- /Screenshots/RefEditUI2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ron-Ldn/DotNetRefEdit/1d8d58ec830cf93683467872e93540fdd5ee1f47/Screenshots/RefEditUI2.png -------------------------------------------------------------------------------- /Screenshots/Ribbon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ron-Ldn/DotNetRefEdit/1d8d58ec830cf93683467872e93540fdd5ee1f47/Screenshots/Ribbon.png -------------------------------------------------------------------------------- /Screenshots/WholeForm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ron-Ldn/DotNetRefEdit/1d8d58ec830cf93683467872e93540fdd5ee1f47/Screenshots/WholeForm.png -------------------------------------------------------------------------------- /Source/DotNetRefEdit.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 11.00 3 | # Visual Studio 2010 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetRefEdit", "DotNetRefEdit\DotNetRefEdit.csproj", "{FF741B2C-EC4D-47A3-8222-913A82CF2340}" 5 | EndProject 6 | Global 7 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 8 | Debug|Any CPU = Debug|Any CPU 9 | Release|Any CPU = Release|Any CPU 10 | EndGlobalSection 11 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 12 | {FF741B2C-EC4D-47A3-8222-913A82CF2340}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 13 | {FF741B2C-EC4D-47A3-8222-913A82CF2340}.Debug|Any CPU.Build.0 = Debug|Any CPU 14 | {FF741B2C-EC4D-47A3-8222-913A82CF2340}.Release|Any CPU.ActiveCfg = Release|Any CPU 15 | {FF741B2C-EC4D-47A3-8222-913A82CF2340}.Release|Any CPU.Build.0 = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(SolutionProperties) = preSolution 18 | HideSolutionNode = FALSE 19 | EndGlobalSection 20 | EndGlobal 21 | -------------------------------------------------------------------------------- /Source/DotNetRefEdit/DotNetRefEdit.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | AnyCPU 6 | 8.0.30703 7 | 2.0 8 | {FF741B2C-EC4D-47A3-8222-913A82CF2340} 9 | Library 10 | Properties 11 | DotNetRefEdit 12 | DotNetRefEdit 13 | v4.0 14 | 512 15 | 16 | 17 | 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | ..\ExcelDna.Integration.dll 37 | 38 | 39 | 40 | True 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | Form 63 | 64 | 65 | RefEditForm.cs 66 | 67 | 68 | 69 | 70 | RefEditWindow.xaml 71 | 72 | 73 | 74 | 75 | 76 | 77 | RefEditForm.cs 78 | 79 | 80 | 81 | 82 | PreserveNewest 83 | 84 | 85 | PreserveNewest 86 | 87 | 88 | PreserveNewest 89 | 90 | 91 | PreserveNewest 92 | 93 | 94 | 95 | 96 | MSBuild:Compile 97 | Designer 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 116 | -------------------------------------------------------------------------------- /Source/DotNetRefEdit/DotNetRefEdit.dna: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /Source/DotNetRefEdit/DotNetRefEdit.xll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ron-Ldn/DotNetRefEdit/1d8d58ec830cf93683467872e93540fdd5ee1f47/Source/DotNetRefEdit/DotNetRefEdit.xll -------------------------------------------------------------------------------- /Source/DotNetRefEdit/DotNetRefEdit64.dna: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /Source/DotNetRefEdit/DotNetRefEdit64.xll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ron-Ldn/DotNetRefEdit/1d8d58ec830cf93683467872e93540fdd5ee1f47/Source/DotNetRefEdit/DotNetRefEdit64.xll -------------------------------------------------------------------------------- /Source/DotNetRefEdit/ExcelHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Runtime.InteropServices; 3 | using Microsoft.Office.Interop.Excel; 4 | 5 | namespace DotNetRefEdit 6 | { 7 | /// 8 | /// Common functions used by the form and the WPF window 9 | /// 10 | static class ExcelHelper 11 | { 12 | /// 13 | /// Make Excel evaluate the formula. 14 | /// To be run in Excel thread. 15 | /// 16 | /// 17 | /// 18 | /// 19 | public static object EvaluateFormula(string formula, Application application) 20 | { 21 | try 22 | { 23 | object formulaResult = application.Evaluate(formula); 24 | 25 | // Check the Excel error codes 26 | if (formulaResult is int) 27 | { 28 | switch ((int)formulaResult) 29 | { 30 | case -2146826288: 31 | case -2146826281: 32 | case -2146826265: 33 | case -2146826259: 34 | case -2146826252: 35 | case -2146826246: 36 | case -2146826273: 37 | return "Could not evaluate function"; 38 | } 39 | } 40 | 41 | return formulaResult; 42 | } 43 | catch 44 | { 45 | return "Could not evaluate function"; 46 | } 47 | } 48 | 49 | /// 50 | /// Insert formula into Excel range. 51 | /// To be run in Excel thread. 52 | /// 53 | /// 54 | /// 55 | /// 56 | public static void InsertFormula(string formula, Application application, string destination) 57 | { 58 | Range rg = null; 59 | 60 | try 61 | { 62 | rg = application.Range[destination]; 63 | rg.Formula = formula; 64 | } 65 | finally 66 | { 67 | if (rg != null) 68 | { 69 | Marshal.ReleaseComObject(rg); 70 | } 71 | } 72 | } 73 | 74 | /// 75 | /// Try to switch the address format, following this sequence: 76 | /// 1. RowAbsolute=False, ColumnAbsolute=False 77 | /// 2. RowAbsolute=True, ColumnAbsolute=True 78 | /// 3. RowAbsolute=True, ColumnAbsolute=False 79 | /// 4. RowAbsolute=False, ColumnAbsolute=True 80 | /// This shall reproduce the behaviour of the Excel "Function Arguments" form when the user hits F4. 81 | /// To be run in Excel thread. 82 | /// 83 | /// 84 | /// 85 | /// 86 | /// 87 | public static bool TryF4(string text, Application application, out string newAddress) 88 | { 89 | try 90 | { 91 | object formulaResult = application.Evaluate(text); 92 | 93 | if (formulaResult is Range) 94 | { 95 | string relativePart = text; 96 | 97 | if (text.Contains("!")) 98 | { 99 | relativePart = text.Substring(text.IndexOf("!") + 1, text.Length - text.IndexOf("!") - 1); 100 | } 101 | 102 | Range range = (Range) formulaResult; 103 | 104 | List addresses = new List 105 | { 106 | range.Address[false, false, XlReferenceStyle.xlA1, false], 107 | range.Address[true, true, XlReferenceStyle.xlA1, false], 108 | range.Address[true, false, XlReferenceStyle.xlA1, false], 109 | range.Address[false, true, XlReferenceStyle.xlA1, false] 110 | }; 111 | 112 | bool found = false; 113 | for (int i = 0; i < addresses.Count; i++) 114 | { 115 | if (addresses[i] == relativePart) 116 | { 117 | relativePart = addresses[i + 1 == addresses.Count ? 0 : i + 1]; 118 | found = true; 119 | break; 120 | } 121 | } 122 | 123 | if (!found) 124 | { 125 | newAddress = range.Address[false, false, XlReferenceStyle.xlA1, true]; 126 | return true; 127 | } 128 | 129 | newAddress = text.Contains("!") 130 | ? string.Concat(text.Substring(0, text.IndexOf("!") + 1), relativePart) 131 | : relativePart; 132 | 133 | return true; 134 | } 135 | 136 | newAddress = null; 137 | return false; 138 | } 139 | catch 140 | { 141 | newAddress = null; 142 | return false; 143 | } 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Source/DotNetRefEdit/ExcelSelectionTracker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Runtime.InteropServices; 4 | using System.Text; 5 | using ExcelDna.Integration; 6 | using Microsoft.Office.Interop.Excel; 7 | using Application = Microsoft.Office.Interop.Excel.Application; 8 | 9 | namespace DotNetRefEdit 10 | { 11 | public class RangeAddressEventArgs : EventArgs 12 | { 13 | public string Address { get; set; } 14 | } 15 | 16 | public class ExcelSelectionTracker 17 | { 18 | private readonly Application _application; 19 | 20 | public event EventHandler NewSelection; 21 | 22 | private readonly int _hHookCwp; 23 | private readonly WindowsInterop.HookProc _procCwp; // Note: do not make this delegate a local variable within the ExcelSelectionTracker constructor because it must not be collected by the GC before the unhook 24 | 25 | public ExcelSelectionTracker(int excelThreadId) 26 | { 27 | _application = (Application)ExcelDnaUtil.Application; 28 | _application.SheetSelectionChange += OnNewSelection; 29 | 30 | _procCwp = CwpProc; 31 | 32 | _hHookCwp = WindowsInterop.SetWindowsHookEx(HookType.WH_CALLWNDPROC, _procCwp, (IntPtr)0, excelThreadId); 33 | if (_hHookCwp == 0) 34 | { 35 | throw new Exception("Failed to hook WH_CALLWNDPROC"); 36 | } 37 | } 38 | 39 | public void Stop() 40 | { 41 | _application.SheetSelectionChange -= OnNewSelection; 42 | 43 | if (!WindowsInterop.UnhookWindowsHookEx(_hHookCwp)) 44 | { 45 | Debug.Print("Error: Failed to unhook WH_CALLWNDPROC"); 46 | } 47 | } 48 | 49 | private int CwpProc(int nCode, IntPtr wParam, IntPtr lParam) 50 | { 51 | CwpStruct cwpStruct = (CwpStruct)Marshal.PtrToStructure(lParam, typeof(CwpStruct)); 52 | 53 | if (nCode < 0) 54 | { 55 | return WindowsInterop.CallNextHookEx(_hHookCwp, nCode, wParam, lParam); 56 | } 57 | 58 | if (cwpStruct.message == WindowsInterop.WM_MOUSEACTIVATE) 59 | { 60 | // We got a WM_MOUSEACTIVATE message. Now we will check that the target handle is a workbook window. 61 | // Workbook windows have the name "EXCEL7". 62 | bool isWorkbookWindow = false; 63 | 64 | try 65 | { 66 | StringBuilder cname = new StringBuilder(256); 67 | WindowsInterop.GetClassNameW(cwpStruct.hwnd, cname, cname.Capacity); 68 | if (cname.ToString() == "EXCEL7") 69 | { 70 | isWorkbookWindow = true; 71 | } 72 | } 73 | catch (Exception e) 74 | { 75 | Debug.Print("Could not get the window name: {0}", e); 76 | } 77 | 78 | if (isWorkbookWindow) 79 | { 80 | // If the window is not activated, then Excel will activate it and then discard the message. That's why the user cannot select a range at the same time. 81 | // The following statement will activate the window before Excel treats the message, thus it will not activate the window and it will keep proceeding the message. 82 | // In that way, it is possible to select the range. 83 | try 84 | { 85 | WindowsInterop.SetFocus(cwpStruct.hwnd); 86 | } 87 | catch (Exception e) 88 | { 89 | Debug.Print("Failed to set the focus: {0}", e); 90 | } 91 | 92 | // If the user chooses a cell which was already selected, then the event SheetSelectionChange will not be raised. 93 | // A workaround is to send the current selection when the Excel window gets the focus. 94 | // Note that if the user selects a different range, then 2 events will be raised: a first one with the current selection, 95 | // and a second one with the new selection. 96 | try 97 | { 98 | OnNewSelection(null, (Range) _application.Selection); 99 | } 100 | catch 101 | { 102 | } 103 | } 104 | } 105 | 106 | return WindowsInterop.CallNextHookEx(_hHookCwp, nCode, wParam, lParam); 107 | } 108 | 109 | private void OnNewSelection(object sh, Range target) 110 | { 111 | try 112 | { 113 | var newSelection = NewSelection; 114 | if (newSelection != null) 115 | { 116 | newSelection(this, new RangeAddressEventArgs { Address = target.Address[false, false, XlReferenceStyle.xlA1, true] }); 117 | } 118 | } 119 | catch 120 | { 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Source/DotNetRefEdit/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("DotNetRefEdit")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("DotNetRefEdit")] 13 | [assembly: AssemblyCopyright("")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("fe65e058-3496-4f18-b459-4d22891accbf")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /Source/DotNetRefEdit/RefEditForm.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace DotNetRefEdit 2 | { 3 | partial class RefEditForm 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Windows Form Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | this.InputBox1 = new System.Windows.Forms.RichTextBox(); 32 | this.label1 = new System.Windows.Forms.Label(); 33 | this.InputBox2 = new System.Windows.Forms.RichTextBox(); 34 | this.label2 = new System.Windows.Forms.Label(); 35 | this.label3 = new System.Windows.Forms.Label(); 36 | this.DestinationBox = new System.Windows.Forms.RichTextBox(); 37 | this.label4 = new System.Windows.Forms.Label(); 38 | this.InsertButton = new System.Windows.Forms.Button(); 39 | this.label5 = new System.Windows.Forms.Label(); 40 | this.EvaluationBox = new System.Windows.Forms.RichTextBox(); 41 | this.SuspendLayout(); 42 | // 43 | // InputBox1 44 | // 45 | this.InputBox1.Location = new System.Drawing.Point(62, 44); 46 | this.InputBox1.Multiline = false; 47 | this.InputBox1.Name = "InputBox1"; 48 | this.InputBox1.Size = new System.Drawing.Size(395, 28); 49 | this.InputBox1.TabIndex = 0; 50 | this.InputBox1.Text = ""; 51 | // 52 | // label1 53 | // 54 | this.label1.AutoSize = true; 55 | this.label1.Font = new System.Drawing.Font("Microsoft Sans Serif", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); 56 | this.label1.Location = new System.Drawing.Point(12, 9); 57 | this.label1.Name = "label1"; 58 | this.label1.Size = new System.Drawing.Size(295, 20); 59 | this.label1.TabIndex = 1; 60 | this.label1.Text = "Select input ranges and sum the values !"; 61 | // 62 | // InputBox2 63 | // 64 | this.InputBox2.Location = new System.Drawing.Point(62, 78); 65 | this.InputBox2.Multiline = false; 66 | this.InputBox2.Name = "InputBox2"; 67 | this.InputBox2.Size = new System.Drawing.Size(395, 28); 68 | this.InputBox2.TabIndex = 2; 69 | this.InputBox2.Text = ""; 70 | // 71 | // label2 72 | // 73 | this.label2.AutoSize = true; 74 | this.label2.Location = new System.Drawing.Point(12, 44); 75 | this.label2.Name = "label2"; 76 | this.label2.Size = new System.Drawing.Size(44, 13); 77 | this.label2.TabIndex = 3; 78 | this.label2.Text = "Augend"; 79 | // 80 | // label3 81 | // 82 | this.label3.AutoSize = true; 83 | this.label3.Location = new System.Drawing.Point(12, 78); 84 | this.label3.Name = "label3"; 85 | this.label3.Size = new System.Drawing.Size(44, 13); 86 | this.label3.TabIndex = 4; 87 | this.label3.Text = "Addend"; 88 | // 89 | // DestinationBox 90 | // 91 | this.DestinationBox.Location = new System.Drawing.Point(79, 148); 92 | this.DestinationBox.Multiline = false; 93 | this.DestinationBox.Name = "DestinationBox"; 94 | this.DestinationBox.Size = new System.Drawing.Size(378, 28); 95 | this.DestinationBox.TabIndex = 5; 96 | this.DestinationBox.Text = ""; 97 | // 98 | // label4 99 | // 100 | this.label4.AutoSize = true; 101 | this.label4.Location = new System.Drawing.Point(13, 151); 102 | this.label4.Name = "label4"; 103 | this.label4.Size = new System.Drawing.Size(60, 13); 104 | this.label4.TabIndex = 6; 105 | this.label4.Text = "Destination"; 106 | // 107 | // InsertButton 108 | // 109 | this.InsertButton.BackColor = System.Drawing.SystemColors.ScrollBar; 110 | this.InsertButton.Location = new System.Drawing.Point(79, 194); 111 | this.InsertButton.Name = "InsertButton"; 112 | this.InsertButton.Size = new System.Drawing.Size(219, 30); 113 | this.InsertButton.TabIndex = 7; 114 | this.InsertButton.Text = "Insert"; 115 | this.InsertButton.UseVisualStyleBackColor = false; 116 | this.InsertButton.Click += new System.EventHandler(this.InsertButton_Click); 117 | // 118 | // label5 119 | // 120 | this.label5.AutoSize = true; 121 | this.label5.Location = new System.Drawing.Point(13, 240); 122 | this.label5.Name = "label5"; 123 | this.label5.Size = new System.Drawing.Size(57, 13); 124 | this.label5.TabIndex = 8; 125 | this.label5.Text = "Evaluation"; 126 | // 127 | // EvaluationBox 128 | // 129 | this.EvaluationBox.Location = new System.Drawing.Point(79, 237); 130 | this.EvaluationBox.Name = "EvaluationBox"; 131 | this.EvaluationBox.ReadOnly = true; 132 | this.EvaluationBox.Size = new System.Drawing.Size(378, 28); 133 | this.EvaluationBox.TabIndex = 9; 134 | this.EvaluationBox.Text = ""; 135 | // 136 | // RefEditForm 137 | // 138 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); 139 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 140 | this.ClientSize = new System.Drawing.Size(468, 278); 141 | this.Controls.Add(this.EvaluationBox); 142 | this.Controls.Add(this.label5); 143 | this.Controls.Add(this.InsertButton); 144 | this.Controls.Add(this.label4); 145 | this.Controls.Add(this.DestinationBox); 146 | this.Controls.Add(this.label3); 147 | this.Controls.Add(this.label2); 148 | this.Controls.Add(this.InputBox2); 149 | this.Controls.Add(this.label1); 150 | this.Controls.Add(this.InputBox1); 151 | this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow; 152 | this.MaximizeBox = false; 153 | this.MaximumSize = new System.Drawing.Size(474, 302); 154 | this.MinimizeBox = false; 155 | this.MinimumSize = new System.Drawing.Size(474, 302); 156 | this.Name = "RefEditForm"; 157 | this.Text = "RefEdit"; 158 | this.ResumeLayout(false); 159 | this.PerformLayout(); 160 | 161 | } 162 | 163 | #endregion 164 | 165 | private System.Windows.Forms.RichTextBox InputBox1; 166 | private System.Windows.Forms.Label label1; 167 | private System.Windows.Forms.RichTextBox InputBox2; 168 | private System.Windows.Forms.Label label2; 169 | private System.Windows.Forms.Label label3; 170 | private System.Windows.Forms.RichTextBox DestinationBox; 171 | private System.Windows.Forms.Label label4; 172 | private System.Windows.Forms.Button InsertButton; 173 | private System.Windows.Forms.Label label5; 174 | private System.Windows.Forms.RichTextBox EvaluationBox; 175 | } 176 | } -------------------------------------------------------------------------------- /Source/DotNetRefEdit/RefEditForm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Forms; 3 | using ExcelDna.Integration; 4 | using Application = Microsoft.Office.Interop.Excel.Application; 5 | 6 | namespace DotNetRefEdit 7 | { 8 | public partial class RefEditForm : Form 9 | { 10 | private readonly ExcelSelectionTracker _selectionTracker; 11 | private readonly Application _application; 12 | private RichTextBox _focusedBox; 13 | 14 | public RefEditForm(int excelThreadId) 15 | { 16 | InitializeComponent(); 17 | _selectionTracker = new ExcelSelectionTracker(excelThreadId); 18 | _application = (Application)ExcelDnaUtil.Application; 19 | 20 | Closed += delegate 21 | { 22 | _selectionTracker.NewSelection -= ChangeText; 23 | _selectionTracker.Stop(); 24 | }; 25 | 26 | _selectionTracker.NewSelection += ChangeText; 27 | 28 | Deactivate += CheckFocus; 29 | 30 | InputBox1.TextChanged += OnNewInput; 31 | InputBox2.TextChanged += OnNewInput; 32 | 33 | InputBox1.KeyDown += CheckF4; 34 | InputBox2.KeyDown += CheckF4; 35 | DestinationBox.KeyDown += CheckF4; 36 | } 37 | 38 | private void CheckFocus(object sender, EventArgs eventArgs) 39 | { 40 | if (InputBox1.Focused) 41 | { 42 | _focusedBox = InputBox1; 43 | } 44 | else if (InputBox2.Focused) 45 | { 46 | _focusedBox = InputBox2; 47 | } 48 | else if (DestinationBox.Focused) 49 | { 50 | _focusedBox = DestinationBox; 51 | } 52 | else 53 | { 54 | _focusedBox = null; 55 | } 56 | } 57 | 58 | /// 59 | /// Build final formula: to be run in UI thread 60 | /// 61 | /// 62 | private string BuildFormula() 63 | { 64 | return string.Format("=sum({0},{1})", InputBox1.Text, InputBox2.Text); 65 | } 66 | 67 | /// 68 | /// Evaluate the formula: to be run in Excel thread 69 | /// 70 | private void UpdateEvaluation(string formula) 71 | { 72 | object formulaResult = ExcelHelper.EvaluateFormula(formula, _application); 73 | Invoke(new Action(() => EvaluationBox.Text = (formulaResult ?? "").ToString())); 74 | } 75 | 76 | private void OnNewInput(object sender, EventArgs e) 77 | { 78 | string formula = BuildFormula(); 79 | ExcelAsyncUtil.QueueAsMacro(() => UpdateEvaluation(formula)); 80 | } 81 | 82 | private void ChangeText(object sender, RangeAddressEventArgs args) 83 | { 84 | Invoke(new Action(() => 85 | { 86 | if (_focusedBox != null) 87 | { 88 | _focusedBox.Text = args.Address; 89 | _focusedBox.Select(_focusedBox.Text.Length, 0); 90 | } 91 | })); 92 | } 93 | 94 | private void CheckF4(object sender, KeyEventArgs e) 95 | { 96 | RichTextBox textBox = sender as RichTextBox; 97 | if (e.KeyCode == Keys.F4 && textBox != null) 98 | { 99 | string text = textBox.Text; 100 | 101 | ExcelAsyncUtil.QueueAsMacro(() => 102 | { 103 | string newAddress; 104 | if (ExcelHelper.TryF4(text, _application, out newAddress)) 105 | { 106 | Invoke(new Action(() => 107 | { 108 | textBox.Text = newAddress; 109 | textBox.Select(textBox.Text.Length, 0); 110 | })); 111 | } 112 | }); 113 | } 114 | } 115 | 116 | private void InsertButton_Click(object sender, EventArgs e) 117 | { 118 | string formula = BuildFormula(); 119 | string destination = DestinationBox.Text; 120 | 121 | if (formula != null && !string.IsNullOrEmpty(destination)) 122 | { 123 | ExcelAsyncUtil.QueueAsMacro(() => ExcelHelper.InsertFormula(formula, _application, destination)); 124 | } 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Source/DotNetRefEdit/RefEditForm.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | -------------------------------------------------------------------------------- /Source/DotNetRefEdit/RefEditWindow.xaml: -------------------------------------------------------------------------------- 1 |  6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | 40 | 41 | 44 | 45 | 46 | 47 | 48 | 49 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |