Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 6개 있습니다.)
VS.NET IDE: 77. Visual Studio 확장(VSIX) 만드는 방법
; https://www.sysnet.pe.kr/2/0/1515

VS.NET IDE: 78. Visual Studio 확장으로 XmlCodeGenerator 제작하는 방법
; https://www.sysnet.pe.kr/2/0/1518

VS.NET IDE: 115. Visual Studio 확장(VSIX)을 이용해 사용자 메뉴 추가하는 방법
; https://www.sysnet.pe.kr/2/0/11184

VS.NET IDE: 116. Visual Studio 확장(VSIX)을 이용해 사용자 메뉴 추가하는 방법 (2) - 동적 메뉴 구성
; https://www.sysnet.pe.kr/2/0/11185

VS.NET IDE: 117. Visual Studio 확장(VSIX)을 이용해 사용자 매크로를 추가하는 방법
; https://www.sysnet.pe.kr/2/0/11186

VS.NET IDE: 165. Visual Studio 2022를 위한 Extension 마이그레이션
; https://www.sysnet.pe.kr/2/0/12682




Visual Studio 확장(VSIX)을 이용해 사용자 매크로를 추가하는 방법

지난 2개의 글을 통해 Visual Studio에 사용자 정의 메뉴를 추가하는 방법을 알아봤습니다.

Visual Studio 확장(VSIX)을 이용해 사용자 메뉴 추가하는 방법
; https://www.sysnet.pe.kr/2/0/11184

Visual Studio 확장(VSIX)을 이용해 사용자 메뉴 추가하는 방법 (2) - 동적 메뉴 구성
; https://www.sysnet.pe.kr/2/0/11185

사실, 위의 2개 글만 해도 "매크로" 기능을 추가하는 것은 구현된 것이나 다름없습니다. 왜냐하면, 매크로 함수 자체는 "EnvDTE80.DTE2 dte" 객체만 알고 있으면 대부분의 기능을 구현할 수 있는 데다 단지 메뉴와 연결해 주면 끝이기 때문입니다.

그런데, 여기서 한 가지 문제가 있습니다. 바로 "단축키"의 구현입니다. 자주 사용하는 매크로를 일일이 메뉴로 선택하는 것은 불편하기 때문에 단축키의 구현이 필수인데요. 문제는, 동적 메뉴의 경우 단축키를 위한 설정이 Visual Studio에는 제공되지 않는다는 점입니다. 이 때문에 다음과 같은 Q&A가 나오는데요.

Can one assign keyboard shortcuts to Visual Studio 2012 extensibility package commands that use DynamicItemStart?
; http://stackoverflow.com/questions/15487894/can-one-assign-keyboard-shortcuts-to-visual-studio-2012-extensibility-package-co

해결 방법을 보면, 동적 메뉴 스타일이 아닌 평범한 메뉴를 100개 정도 .vsct에 등록한 후 OnBeforeQueryStatus 콜백에서 show/hide를 하는 식으로 처리하라는 것입니다.

실제로, Visual Studio Marketplace에 등록된 매크로 확장인 Visual Commander를 보면,

Visual Commander 
; https://marketplace.visualstudio.com/items?itemName=SergeyVlasov.VisualCommander
; https://vlasovstudio.com/visual-commander/

소개 페이지에 다음과 같은 문구를 볼 수 있습니다.

  • the free edition supports only 10 commands and 5 extensions
  • The Professional Edition supports up to 99 commands and 50 extensions

상용인 Pro 버전까지도 등록 가능한 명령어의 수를 99개로 제한하고 있는 것입니다. 아마도 제 생각에는, 단축키 문제만 아니었다면 동적 메뉴를 구성하면 되므로 100 개 이상의 명령어 지원이 더 쉬웠을 것입니다.

자, 그래서 우리가 지금까지 구현했던 동적 메뉴가 단축키 구현으로 인해 쓸모 없게 되었습니다. 이제는 다시 첫 번째 글로 돌아가서 100개 정도의 dummy 메뉴를 등록하는 것으로 시작해야 합니다.

첫 단계로 기본 그룹을 부모로 하는 메뉴를 하나 등록하고,

<Menus>
    <Menu guid="guidMacroCommandPackageCmdSet" id="MyMenu" priority="0x1000" type="Menu">
    <Parent guid="guidMacroCommandPackageCmdSet" id="MyMenuGroup" />
    <CommandFlag>AlwaysCreate</CommandFlag>
    <Strings>
        <ButtonText>MyMacro List</ButtonText>
    </Strings>
    </Menu>
</Menus>    

그것의 서브 메뉴로 펼쳐질 "임시 메뉴"를 많이(?) 등록합니다. (우리는 예제를 위한 것이니, 4개의 dummy 메뉴만 만들겠습니다.)

<Buttons>
    <Button guid="guidMacroCommandPackageCmdSet" id="MacroCommandId_1" priority="0x0100" type="Button">
    <Parent guid="guidMacroCommandPackageCmdSet" id="MySubMenuGroup" />
    <CommandFlag>DynamicVisibility</CommandFlag>
    <CommandFlag>TextChanges</CommandFlag>
    <Strings>
        <ButtonText>CMD 1</ButtonText>
    </Strings>
    </Button>
    
    <Button guid="guidMacroCommandPackageCmdSet" id="MacroCommandId_2" priority="0x0100" type="Button">
    <Parent guid="guidMacroCommandPackageCmdSet" id="MySubMenuGroup" />
    <CommandFlag>DynamicVisibility</CommandFlag>
    <CommandFlag>TextChanges</CommandFlag>
    <Strings>
        <ButtonText>CMD 2</ButtonText>
    </Strings>
    </Button>
      
    <Button guid="guidMacroCommandPackageCmdSet" id="MacroCommandId_3" priority="0x0100" type="Button">
    <Parent guid="guidMacroCommandPackageCmdSet" id="MySubMenuGroup" />
    <CommandFlag>DynamicVisibility</CommandFlag>
    <CommandFlag>TextChanges</CommandFlag>
    <Strings>
        <ButtonText>CMD 3</ButtonText>
    </Strings>
    </Button>      
      
    <Button guid="guidMacroCommandPackageCmdSet" id="MacroCommandId_4" priority="0x0100" type="Button">
    <Parent guid="guidMacroCommandPackageCmdSet" id="MySubMenuGroup" />
    <CommandFlag>DynamicVisibility</CommandFlag>
    <CommandFlag>TextChanges</CommandFlag>
    <Strings>
        <ButtonText>CMD 4</ButtonText>
    </Strings>
    </Button>      
</Buttons>

마지막으로 위의 설정들에 대한 ID 값들을 GuidSymbol에 등록합니다.

<GuidSymbol name="guidMacroCommandPackageCmdSet" value="{abe0296d-5394-4dff-8453-5440c28f4696}">
    <IDSymbol name="MyMenuGroup" value="0x1020" />
    <IDSymbol name="MySubMenuGroup" value="0x1030" />
    <IDSymbol name="MyMenu" value="0x2000" />
    <IDSymbol name="MacroCommandId_1" value="0x3001" />
    <IDSymbol name="MacroCommandId_2" value="0x3002" />
    <IDSymbol name="MacroCommandId_3" value="0x3003" />
    <IDSymbol name="MacroCommandId_4" value="0x3004" />
</GuidSymbol>

여기까지의 .vsct 파일 변경만 하고 실행해 보면, "Tools" 메뉴에 "MyMacro List" 메뉴가 생성되고, 그 서브 메뉴로 "CMD 1", "CMD 2", "CMD 3, "CMD 4"가 생성된 것을 확인할 수 있습니다.




이후의 구현은 매크로 등록 방법만 여러분들이 임의로 결정하시면 됩니다. 매크로를 선택했을 때 실행될 명령어를 C# 소스 코드 파일로 유지하고 그 정보를 동적으로 로딩해 메뉴를 구성할 수 있습니다. 이 예제에서는 4개의 메뉴를 미리 생성해 두었는데, 가령 2개의 매크로를 여러분들이 정의했다고 가정하면 다음과 같은 식으로 코딩할 수 있습니다.

private MacroCommand(Package package)
{
    if (package == null)
    {
        throw new ArgumentNullException("package");
    }

    this.package = package;
    dte2 = (DTE2)this.ServiceProvider.GetService(typeof(DTE));

    OleMenuCommandService commandService = this.ServiceProvider.GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
    if (commandService != null)
    {
        for (int i = 0; i < 4; i++)
        {
            var menuCommandID = new CommandID(CommandSet, CommandId + i);
            var menuItem = new OleMenuCommand(this.MenuItemCallback, menuCommandID);
            menuItem.BeforeQueryStatus += MenuItem_BeforeQueryStatus;
            commandService.AddCommand(menuItem);
        }
    }
}

private void MenuItem_BeforeQueryStatus(object sender, EventArgs e)
{
    OleMenuCommand menu = sender as OleMenuCommand;
    if (menu != null)
    {
        if (menu.CommandID.ID >= 0x3003) // 3번째 메뉴 이후의 것을 비활성
        {
            menu.Enabled = false;
            menu.Visible = false;
            return;
        }

        if (menu.CommandID.ID == 0x3001)
        {
            menu.Text = "MakeAHref"; // 첫 번째 메뉴
        }
    }
}

또한, 2개의 매크로 명령이 선택되었을 때 사용자가 입력해 두었던 C# 소스 코드를 동적으로 컴파일한 다음 delegate로 읽어들여 DTE2 dte2 객체를 인자로 넘겨주면 됩니다. 예를 들어, "Visual Commander"의 경우 다음과 같은 식으로 소스 코드를 정의하도록 하고 있습니다.

using EnvDTE;
using EnvDTE80;

public class C : VisualCommanderExt.ICommand
{
    public void Run(EnvDTE80.DTE2 dte, Microsoft.VisualStudio.Shell.Package package) 
    {
    }
}

결국, 매크로의 모든 동작이 dte/package 객체만 있으면 된다는 것인데 일단 이 글에서는 그 모든 것을 구현할 수는 없고 메뉴가 선택되었을 때 내부 함수를 실행하는 것으로 다음과 같이 구현해 봤습니다.

private void MenuItemCallback(object sender, EventArgs e)
{
    MenuCommand menu = sender as MenuCommand;

    if (menu != null)
    {
        RunCommand(menu.CommandID.ID);
    }
}

private void RunCommand(int id)
{
    switch (id)
    {
        case 0x3001:
            MakeAHref(this.dte2, this.package);
            break;
    }
}

위의 MakeAHref 메서드는 사용자가 선택한 텍스트 영역을 <a />로 감싸는 문자열을 생성해 줍니다.

private void MakeAHref(EnvDTE80.DTE2 dte, Package package)
{
    if (dte.ActiveDocument == null)
    {
        return;
    }

    TextSelection selection = dte.ActiveDocument.Selection as TextSelection;

    var str = "<a target='tab' href='" + DoHtmlEncode(selection.Text) + "'>" + selection.Text + "</a>";

    SetText(dte, selection, str);
}

public string DoHtmlEncode(string text)
{
    text = text.Replace("&", "&amp;");
    text = text.Replace("<", "&lt;");
    text = text.Replace(">", "&gt;");

    return text;
}

public void SetText(EnvDTE80.DTE2 dte, TextSelection iSelection, string iText)
{
    try
    {
        dte.UndoContext.Open("SetTextMakeAHref");
    }
    catch
    {
        dte.UndoContext.SetAborted();
        return;
    }

    var prop = dte.DTE.Properties["TextEditor", "PlainText"].Item("IndentStyle");
    var oldIndent = prop.Value;
    prop.Value = 0;

    iSelection.Text = iText;

    prop.Value = oldIndent;

    dte.UndoContext.Close();
}

자, 이제 빌드하고 실행하면 테스트 환경의 Visual Studio가 뜨고 다음과 같이 2개의 메뉴를 볼 수 있습니다.

macro_menu_1.png

텍스트 파일 하나 열어서 다음과 같은 식으로 URL을 입력한 다음 문자열을 선택한 후,

macro_menu_2.png

"Tools" / "MyMacro List"의 "MakeAHref" 명령어를 선택해 주면 이렇게 바뀝니다.

macro_menu_3.png




이쯤에서 잠깐 잊고 있었던 단축키 이야기로 돌아가 보겠습니다. 위와 같이 코딩한 상태에서 "Tools" / "Options" 메뉴를 선택해 "Keyboard"로 가보면 다음과 같이 단축키 목록이 나옵니다.

macro_menu_4.png

물론 해당 메뉴에 대해 단축키를 지정할 수 있습니다. 그런데, 여기서 새로운 문제가 있는데 모든 명령어의 이름이 ".vsct"에 등록된 문자열로만 나온다는 점입니다. 즉, BeforeQueryStatus에서 바꾼 현재의 메뉴 이름이 반영되지 않고 있는데, 저도 이 해결 방법은 아직 모르겠습니다. 하지만 방법은 분명히 있는 것 같습니다. ^^ 실제로 "Visual Commander"의 유료 버전을 보면 그 이름을 바꿔서 표현하는 기능이 있습니다. (혹시, 이 글을 보시는 분 중에 방법을 아시는 분은 덧글 부탁드립니다. ^^)




만약 범용성을 포기한다면 자신만의 매크로 명령어 확장(VSIX)을 이 글을 통해 쉽게 만드실 수 있을 것입니다. 게다가 단축키 명령어도 처음부터 특정 명령어에 고정해서 미리 제공하는 것이 가능합니다. 방법은 .vsct 파일에 다음과 같이 원하는 메뉴의 명령어 ID에 해당하는 KeyBinding을 지정하면 됩니다. 가령, 다음과 같이 정의해주면,

<KeyBindings>
    <KeyBinding guid="guidMacroCommandPackageCmdSet" id="MacroCommandId_1" editor="guidVSStd97" mod1="Control" key1="E" key2="1" />
    <KeyBinding guid="guidMacroCommandPackageCmdSet" id="MacroCommandId_2" editor="guidVSStd97" mod1="Control" key1="E" key2="2" />
    <KeyBinding guid="guidMacroCommandPackageCmdSet" id="MacroCommandId_3" editor="guidVSStd97" mod1="Control" key1="E" key2="3" />
    <KeyBinding guid="guidMacroCommandPackageCmdSet" id="MacroCommandId_4" editor="guidVSStd97" mod1="Control" key1="E" key2="4" />
</KeyBindings>

4개의 명령어에 대해 각각 "Ctrl + E, 1", "Ctrl + E, 2", "Ctrl + E, 3", "Ctrl + E, 4"에 대해 단축키가 부여됩니다.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 4/24/2017]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2017-08-22 03시41분
squaredinfinity/VSCommands
; https://github.com/squaredinfinity/VSCommands
정성태

... 166  167  168  169  170  171  172  173  174  175  176  177  [178]  179  180  ...
NoWriterDateCnt.TitleFile(s)
530정성태9/1/200722607.NET Framework: 94. WCF 예외에 대한 시행착오
529정성태8/31/200725435.NET Framework: 93. WCF - DataContract와 KnownType 특성 [1]
528정성태8/30/200720097오류 유형: 47. VPC - 네트워크 어댑터 MAC 주소 중복 오류
527정성태8/30/200730107Team Foundation Server: 20. 잠긴 파일을 강제로 해제 [2]
526정성태8/29/200720030오류 유형: 46. VS.NET 2008 - ASP.NET 디버깅 : Strong name validation failed.
525정성태8/27/200722320VS.NET IDE: 54. VS.NET 2008 - 새롭게 도입되는 XSD Schema Designer
524정성태8/23/200739835오류 유형: 45. 요청한 작업은, 사용자가 매핑한 구역이 열려 있는...
523정성태8/16/200722488VS.NET IDE: 53. VS.NET 2008 - 서비스 참조 시 기존 데이터 컨테이너 DLL 사용
522정성태8/13/200726113VS.NET IDE: 52. VS.NET 2008 - WCF를 위한 디버깅 환경 개선
521정성태8/8/200726241.NET Framework: 92. XmlSerializer 생성자의 실행 속도를 올리는 방법 - 두 번째 이야기 [3]
520정성태8/7/200721369VS.NET IDE: 51. Visual Studio 2008 베타 2 설치
519정성태7/27/200727739오류 유형: 44. System.BadImageFormatException [2]
518정성태7/26/200728696오류 유형: 43. System.ComponentModel.LicenseException [1]
517정성태7/19/200716975개발 환경 구성: 26. VPC - 일반 사용자 계정으로 구동
516정성태7/19/200720259오류 유형: 42. TFS - Error loading menu: Index was outside the bounds of the array [2]
515정성태7/18/200727928오류 유형: 41. SSL 서버 자격 증명을 만드는 동안 심각한 오류가 발생했습니다.
514정성태7/14/200720589Team Foundation Server: 19. Orcas에서 개선되는 TFS 기능들
513정성태7/4/200731578.NET Framework: 91. Foreground Thread / Background Thread [1]
512정성태6/27/200721638오류 유형: 40. error PRJ0050: Failed to register output.
511정성태6/25/200729571.NET Framework: 90. XmlSerializer 생성자의 실행 속도를 올리는 방법 [2]
510정성태6/25/200744449디버깅 기술: 15. First-Chance Exception
508정성태6/21/200727414Team Foundation Server: 18. Team Build에 사용되는 각종 Property 값 [4]
507정성태6/11/200725031VS.NET IDE: 50. Orcas - UAC 설정 관련
506정성태6/9/200720216오류 유형: 39. VC Package not available or not registered
505정성태6/9/200719803오류 유형: 38. Visual SourceSafe - DB 잠김 오류
504정성태6/9/200725119오류 유형: 37. Visual SourceSafe - Anaylze 도중 비정상 종료
... 166  167  168  169  170  171  172  173  174  175  176  177  [178]  179  180  ...