乘風破浪,遇見最美Windows 11之現代Windows桌面應用開發 - Visual Studio擴展開發


工欲善其事,必先利其器

通過使用和生成擴展,打造適合你和你的團隊的完美工具。

image

https://visualstudio.microsoft.com/zh-hans/

什么是Visual Studio擴展

https://visualstudio.microsoft.com/zh-hans/vs/features/extend/

Visual Studio擴展(Visual Studio Extend)是可以允許你在Visual Studio中進行自定義並增強在其中的體驗的附加項,通過添加新功能或集成現有工具實現。擴展復雜程度不一,但是其主要用途在於提高工作效率並滿足工作流需求。

image

Visual Studio擴展商店

https://marketplace.visualstudio.com

Visual Studio擴展商店(Visual Studio Marketplace)提供的數千萬個擴展,可以幫助你查找所需工具,增強你的Visual Studio,還可以通過它發布你的擴展。

創建Visual Studio擴展

https://docs.microsoft.com/visualstudio/extensibility/starting-to-develop-visual-studio-extensions

安裝Visual Studio SDK

Visual Studio軟件開發工具包(Visual Studio SDK)是安裝Visual Studio時一個可選項,我們可以通過Visual Studio Installer安裝器,修改首次安裝VS對應版本時,選中並勾選其他工具集分類中的Visual Studio 擴展開發工作負荷來安裝它。

image

或者,如果你本地有擴展的項目,雙擊打開的時候,也會提示你安裝此依賴項。

image

思考Visual Studio能完成的事情

如果你正好有如下所列方向的需求,那么創建Visual Studio擴展可能是比較好的選項之一:

  • 支持未包含在Visual Studio中的語言、語法着色、IntelliSense以及編譯器和調試支持
  • 利用附加模板、代碼重構、新對話框或工具窗口擴展核心IDE體驗的生產力工具
  • 適用於各種方案(如數據設計或雲支持)的域特定設計器

值得留意的是,許多擴展是開放源代碼的,並且Visual Studio擴展商店(Visual Studio Marketplace)包含指向其GitHub存儲庫的鏈接。

image

利用Visual Studio擴展能完成的功能

理論上,你可以只擴展VisualStudio的任何部分:菜單、工具欄、命令、窗口、解決方案、項目、編輯器等。

實際上,我們發現大多數人要擴展的功能是命令、菜單和工具欄、Windows、IntelliSense和項目。下面是相關資料鏈接:

編寫自己的第一個擴展

https://github.com/TaylorShi/demoForVsixExtend

a. 打開Visual Studio創建項目

這里篩選C#語言、所有平台、擴展項目類型,比如這里找到VSIX Project這種項目模板類型,然后點擊"下一步"按鈕。

image

b. 輸入項目名稱,創建項目

這里我們就采用demoForVsixExtend作為項目名稱和解決方案的名稱吧,然后點擊"創建"按鈕。

image

c. 創建成功並打開解決方案

image

d. 查看並編輯擴展元數據

demoForVsixExtend項目中找到.vsixmanifest文件,雙擊可以進入配置界面,這里面編輯擴展的描述、語言、協議、圖標、預覽圖、標簽、更新內容、開始指引、擴展鏈接、是否為預覽拓展。

image

e. 編譯並生成,查看輸出文件

demoForVsixExtend項目的包引用來看,我們可以清晰看到是依賴Microsoft.VisualStudio.SDKMicrosoft.VSSDK.BuildTools兩個包。

全部編譯之后,我們看看Bin生成目錄,可以看到,它會生成一個DLL,和一個.vsix的包。

image

向工具欄添加自定義命令

a. 創建擴展的自定義命令類文件

demoForVsixExtend項目上右鍵,"添加"-"新建項"

image

在已安裝的Visual C#項中找到Extendsibility分組,選擇Command項,我們取個名字叫FirstCommand,然后點擊"添加"按鈕。

image

添加成功之后,我們會發現它會自動創建一個FirstCommand.cs分類,並且在Resources文件夾中,創建配套的FirstCommand.png資源文件。

image

同時,我們還發現自動創建了一個和項目名稱相關的.vsct文件,這個文件是定義我們想添加的自定義命令的地方,用於描述命令位於工具菜單的位置、名稱、圖標等信息。

image

我們單擊打開這個文件,可以看到里面已經描述了一個Button按鈕和其關聯的圖標資源。

image

.vsct這個xml格式文件中,標記了一個Button(FirstCommandId),它的父級指向了一個Group(MyMenuGroup),而這個父級Group的父級指向了頂層的"菜單-工具"(IDM_VS_MENU_TOOLS),並且這個命令的內容文本為Say Hello VsixExtend,且指定了一個圖標資源(bmpPic1)。

<Groups>
    <Group guid="guiddemoForVsixExtendPackageCmdSet" id="MyMenuGroup" priority="0x0600">
    <Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS"/>
    </Group>
</Groups>

<Buttons>
    <Button guid="guiddemoForVsixExtendPackageCmdSet" id="FirstCommandId" priority="0x0100" type="Button">
    <Parent guid="guiddemoForVsixExtendPackageCmdSet" id="MyMenuGroup" />
    <Icon guid="guidImages" id="bmpPic1" />
    <Strings>
        <ButtonText>Say Hello VsixExtend</ButtonText>
    </Strings>
    </Button>
</Buttons>

值得注意的是,一個Button必須歸屬到一個Group下面,而一個Group必須歸屬到頂層的"菜單-工具"里面。

b. 調試自定義命令,查看附加效果

我們啟動調試之后,它會再啟動另外一個Visual Studio的實例,並且附加了調試,最終我們能在"工具"菜單找到我們新增的Invove FirstCommand菜單項入口。

image

c. 修改自定義命令文案信息

我們可以試着修改.vsct文件的內容來自定義按鈕文案,比如我們將按鈕的文案改成Say Hello VsixExtend

<Buttons>
    <!--To define a menu group you have to specify its ID, the parent menu and its display priority.
        The command is visible and enabled by default. If you need to change the visibility, status, etc, you can use
        the CommandFlag node.
        You can add more than one CommandFlag node e.g.:
            <CommandFlag>DefaultInvisible</CommandFlag>
            <CommandFlag>DynamicVisibility</CommandFlag>
        If you do not want an image next to your command, remove the Icon node /> -->
    <Button guid="guiddemoForVsixExtendPackageCmdSet" id="FirstCommandId" priority="0x0100" type="Button">
    <Parent guid="guiddemoForVsixExtendPackageCmdSet" id="MyMenuGroup" />
    <Icon guid="guidImages" id="bmpPic1" />
    <Strings>
        <ButtonText>Say Hello VsixExtend</ButtonText>
    </Strings>
    </Button>
</Buttons>

並且我們將FirstCommand.cs文件中的Execute函數,將里面的彈窗提示內容文案做一定的修改。

private void Execute(object sender, EventArgs e)
{
    ThreadHelper.ThrowIfNotOnUIThread();
    //string message = string.Format(CultureInfo.CurrentCulture, "Inside {0}.MenuItemCallback()", this.GetType().FullName);
    string message = "Hello Tip";
    string title = "FirstCommand";

    // Show a message box to prove we were here
    VsShellUtilities.ShowMessageBox(
        this.package,
        message,
        title,
        OLEMSGICON.OLEMSGICON_INFO,
        OLEMSGBUTTON.OLEMSGBUTTON_OK,
        OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
}

我們再運行一次看看。

image

點擊Say Hello VsixExtend菜單,發現會按預期彈窗和提示。

image

e. 自定義命令的初始化

在命令的.cs文件FirstCommand.cs中,描述了自定義命令的初始化流程,這里面包含InitializeAsync方法的實現,在它里面有通過FirstCommand的構造函數創建並添加了當前的命令項。

/// <summary>
/// Command handler
/// </summary>
internal sealed class FirstCommand
{

    /// <summary>
    /// Command ID.
    /// </summary>
    public const int CommandId = 0x0100;

    /// <summary>
    /// Command menu group (command set GUID).
    /// </summary>
    public static readonly Guid CommandSet = new Guid("8229a384-5bd5-4a80-9076-ef2b05901d4c");

    /// <summary>
    /// VS Package that provides this command, not null.
    /// </summary>
    private readonly AsyncPackage package;

    /// <summary>
    /// Gets the instance of the command.
    /// </summary>
    public static FirstCommand Instance
    {
        get;
        private set;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="FirstCommand"/> class.
    /// Adds our command handlers for menu (commands must exist in the command table file)
    /// </summary>
    /// <param name="package">Owner package, not null.</param>
    /// <param name="commandService">Command service to add command to, not null.</param>
    private FirstCommand(AsyncPackage package, OleMenuCommandService commandService)
    {
        this.package = package ?? throw new ArgumentNullException(nameof(package));
        commandService = commandService ?? throw new ArgumentNullException(nameof(commandService));

        var menuCommandID = new CommandID(CommandSet, CommandId);
        var menuItem = new MenuCommand(this.Execute, menuCommandID);
        commandService.AddCommand(menuItem);
    }

    /// <summary>
    /// Initializes the singleton instance of the command.
    /// </summary>
    /// <param name="package">Owner package, not null.</param>
    public static async Task InitializeAsync(AsyncPackage package)
    {
        // Switch to the main thread - the call to AddCommand in FirstCommand's constructor requires
        // the UI thread.
        await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken);

        OleMenuCommandService commandService = await package.GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService;
        Instance = new FirstCommand(package, commandService);
    }
}

f. 擴展包的初始化

在擴展包的.cs文件demoForVsixExtendPackage.cs的初始化方法InitializeAsync中,正是調用了FirstCommand類的InitializeAsync方法完成了自定義命令的初始化。

[PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
[Guid(demoForVsixExtendPackage.PackageGuidString)]
[ProvideMenuResource("Menus.ctmenu", 1)]
public sealed class demoForVsixExtendPackage : AsyncPackage
{
    /// <summary>
    /// demoForVsixExtendPackage GUID string.
    /// </summary>
    public const string PackageGuidString = "6c3bfb2c-f8a4-40ff-99fa-caf75412fc61";

    #region Package Members

    /// <summary>
    /// Initialization of the package; this method is called right after the package is sited, so this is the place
    /// where you can put all the initialization code that rely on services provided by VisualStudio.
    /// </summary>
    /// <param name="cancellationToken">A cancellation token to monitor for initialization cancellation, which can occur when VS is shutting down.</param>
    /// <param name="progress">A provider for progress updates.</param>
    /// <returns>A task representing the async work of package initialization, or an already completed task if there is none. Do not return null from this method.</returns>
    protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
    {
        // When initialized asynchronously, the current thread may be a background thread at this point.
        // Do any initialization that requires the UI thread after switching to the UI thread.
        await this.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
        await FirstCommand.InitializeAsync(this);
    }

    #endregion
}

而擴展包的初始化方法InitializeAsync,實際上是繼承了統一的Microsoft.VisualStudio.Shell.AsyncPackage類,對原初始化方法進行了重寫來實現的。

namespace Microsoft.VisualStudio.Shell
{
    [ComVisible(true)]
    public abstract class AsyncPackage
    {
        //
        // 摘要:
        //     The async initialization portion of the package initialization process. This
        //     method is invoked from a background thread.
        //
        // 參數:
        //   cancellationToken:
        //     A cancellation token to monitor for initialization cancellation, which can occur
        //     when VS is shutting down.
        //
        // 返回結果:
        //     A task representing the async work of package initialization, or an already completed
        //     task if there is none. Do not return null from this method.
        protected virtual Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
        {
            return Task.CompletedTask;
        }

        //
        // 摘要:
        //     When overridden by a derived type, may initiate operations with side effects
        //     that are not strictly a part of package load that would belong to Microsoft.VisualStudio.Shell.AsyncPackage.InitializeAsync(System.Threading.CancellationToken,System.IProgress{Microsoft.VisualStudio.Shell.ServiceProgressData})
        //     but that should happen soon after package load (e.g. updating command status).
        //
        // 參數:
        //   cancellationToken:
        //     The package's Microsoft.VisualStudio.Shell.AsyncPackage.DisposalToken.
        //
        // 返回結果:
        //     A task that indicates completion of the extra work.
        //
        // 言論:
        //     This method is invoked on a threadpool thread.
        //     The base Microsoft.VisualStudio.Shell.AsyncPackage.OnAfterPackageLoadedAsync(System.Threading.CancellationToken)
        //     method has an empty implementation, so direct derivations of Microsoft.VisualStudio.Shell.AsyncPackage
        //     needn't call the base method as part of their override method.
        protected virtual Task OnAfterPackageLoadedAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }
    }
}

g. 自定義命令的響應

在命令的.cs文件FirstCommand.cs中,當我們通過構造函數添加自定義命令項時,我們同時傳入了命令對應的響應委托EventHandler事件方法Execute,所以如果點擊自定義命令的話,會進入Execute這個響應流程。

/// <summary>
/// Command handler
/// </summary>
internal sealed class FirstCommand
{
    /// <summary>
    /// Initializes a new instance of the <see cref="FirstCommand"/> class.
    /// Adds our command handlers for menu (commands must exist in the command table file)
    /// </summary>
    /// <param name="package">Owner package, not null.</param>
    /// <param name="commandService">Command service to add command to, not null.</param>
    private FirstCommand(AsyncPackage package, OleMenuCommandService commandService)
    {
        this.package = package ?? throw new ArgumentNullException(nameof(package));
        commandService = commandService ?? throw new ArgumentNullException(nameof(commandService));

        var menuCommandID = new CommandID(CommandSet, CommandId);
        var menuItem = new MenuCommand(this.Execute, menuCommandID);
        commandService.AddCommand(menuItem);
    }

    /// <summary>
    /// This function is the callback used to execute the command when the menu item is clicked.
    /// See the constructor to see how the menu item is associated with this function using
    /// OleMenuCommandService service and MenuCommand class.
    /// </summary>
    /// <param name="sender">Event sender.</param>
    /// <param name="e">Event args.</param>
    private void Execute(object sender, EventArgs e)
    {
        ThreadHelper.ThrowIfNotOnUIThread();
        //string message = string.Format(CultureInfo.CurrentCulture, "Inside {0}.MenuItemCallback()", this.GetType().FullName);
        string message = "Hello Tip";
        string title = "FirstCommand";

        // Show a message box to prove we were here
        VsShellUtilities.ShowMessageBox(
            this.package,
            message,
            title,
            OLEMSGICON.OLEMSGICON_INFO,
            OLEMSGBUTTON.OLEMSGBUTTON_OK,
            OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
    }
}

向解決方案資源管理器添加自定義命令

a. 首先新建自定義命令項

demoForVsixExtend項目上右鍵,"添加"-"新建項",在已安裝的Visual C#項中找到Extendsibility分組,選擇Command項,我們取個名字叫SecondCommand,然后點擊"添加"按鈕。

image

生成后,會幫我們創建SecondCommand.csResources\SecondCommand.png,並且自動將SecondCommand的初始化方法InitializeAsync添加到demoForVsixExtendPackage.cs文件中了。

image

b. 修改自定義命令項的元數據標記

首先我們前往demoForVsixExtendPackage.vsct文件,查看到Symbols節點下的GuidSymbol組,我們將SecondCommand相關的單獨拆出來。

<!-- This is the guid used to group the menu commands together -->
<GuidSymbol name="guiddemoForToolbarPackageCmdSet" value="{10F2415E-17D1-4FC7-89B6-C5CE9E21A197}">
    <IDSymbol name="SecondMenuGroup" value="0x1020" />
    <IDSymbol name="SecondCommandId" value="0x0100" />
</GuidSymbol>

<GuidSymbol name="guidImages1" value="{d4f21307-1f2f-40fa-a3fc-2bc83dd71b7a}" >
    <IDSymbol name="bmpPic1" value="1" />
    <IDSymbol name="bmpPic2" value="2" />
    <IDSymbol name="bmpPicSearch" value="3" />
    <IDSymbol name="bmpPicX" value="4" />
    <IDSymbol name="bmpPicArrows" value="5" />
    <IDSymbol name="bmpPicStrikethrough" value="6" />
</GuidSymbol>

這里我們看到兩組GuidSymbol,后面這個是用於描述圖像的,比較好理解,而第一個是整個命令的元數據,這個name要唯一,並且這個value對應的Guid的值,要和SecondCommand.cs里面定義的值一一對應才行,不然無法識別。

image

接下來我們看到Groups組,該組下面擁有一個新的Group,它的Id也是要額外注意,得唯一,且和GuidSymbol中的定義保持一致:SecondMenuGroup,它的父級指向解決方案資源管理器的標記IDM_VS_TOOL_PROJWIN即可。

<Groups>
    <Group guid="guiddemoForVsixExtendPackageCmdSet" id="MyMenuGroup" priority="0x0600">
    <Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS" />
    </Group>
    <Group guid="guiddemoForToolbarPackageCmdSet" id="SecondMenuGroup" priority="0x0600">
    <Parent guid="guidSHLMainMenu" id="IDM_VS_TOOL_PROJWIN" />
    </Group>
</Groups>

然后我們繼續看Buttons組,該組下面擁有一個新的Button,它的Name的具備唯一性,並且它的Id得唯一,且和GuidSymbol中的定義保持一致:SecondCommandId,並且它的父級需要指向SecondMenuGroup

<Buttons>
    <Button guid="guiddemoForToolbarPackageCmdSet" id="SecondCommandId" priority="0x0100" type="Button">
    <Parent guid="guiddemoForToolbarPackageCmdSet" id="SecondMenuGroup" />
    <Icon guid="guidImages1" id="bmpPic1" />
    <CommandFlag>DefaultInvisible</CommandFlag>
    <CommandFlag>DynamicVisibility</CommandFlag>
    <Strings>
        <ButtonText>Invoke SecondCommand</ButtonText>
    </Strings>
    </Button>
</Buttons>

c. 給自定義命令添加顯影屬性,根據是否打開解決方案自適應

僅僅完成前面的部分,我們會發現,無論是否打開解決方案,這個新增的命令都會出現在解決方案的資源管理器工具欄中,那么我們是否可以做到顯影控制,讓它根據是否打開解決方案自適應呢?當然顯然是可以。

我們首先需要確保在Button的描述里面,追加命令的Flag,一般來說,就在IconString之間即可,我們需要追加DefaultInvisibleDynamicVisibility這兩個命令標記。

<Buttons>
    <Button guid="guiddemoForToolbarPackageCmdSet" id="SecondCommandId" priority="0x0100" type="Button">
    <Parent guid="guiddemoForToolbarPackageCmdSet" id="SecondMenuGroup" />
    <Icon guid="guidImages1" id="bmpPic1" />
    <CommandFlag>DefaultInvisible</CommandFlag>
    <CommandFlag>DynamicVisibility</CommandFlag>
    <Strings>
        <ButtonText>Invoke SecondCommand</ButtonText>
    </Strings>
    </Button>
</Buttons>

然后在.vsct文件中,我們需要在一級標記節點中添加顯影控制描述,一般來說,就在CommandsSymbols之間即可,我們需要追加VisibilityConstraints組,並且含2個VisibilityItem子節點,分別用來處理打開一個或者多個項目的情況。

<Commands>
...
</Commands>

<VisibilityConstraints>
<VisibilityItem guid="guiddemoForToolbarPackageCmdSet"
        id="SecondCommandId"
        context="UICONTEXT_SolutionHasSingleProject" />
<VisibilityItem guid="guiddemoForToolbarPackageCmdSet"
        id="SecondCommandId"
        context="UICONTEXT_SolutionHasMultipleProjects" />
</VisibilityConstraints>

<Symbols>
...
</Symbols>

需要注意的是,這里VisibilityItemguid值和id值都要和GuidSymbol中的定義保持一致才有效。

d. 運行查看效果

  • 當未打開任何項目的時候,解決方案資源管理器是不會出現自定義命令的。

image

  • 當打開一個或者多個項目的時候,這時候解決方案資源管理器才會出現我們的定義的命令。

image

Visual Studio最高一級的組清單

https://docs.microsoft.com/zh-cn/visualstudio/extensibility/internals/guids-and-ids-of-visual-studio-menus?view=vs-2019

通過前面的案例我們已經知道了,我們可以把我們自定義的Group的父級指向Visual Studio自帶的一級組,這樣我們可以把自定義組掛到已有的菜單項上了,前面我們分別介紹了IDM_VS_MENU_TOOLSIDM_VS_TOOL_PROJWIN這兩個父級,那么還有哪些組呢?

  • 若要向菜單欄添加新的菜單,可將組的父級設置為如下值:
ID
文件/編輯/查看 IDG_VS_MM_FILEEDITVIEW
重構 IDG_VS_MM_REFACTORING
項目 IDG_VS_MM_PROJECT
構建 IDG_VS_MM_BUILDDEBUGRUN
格式/工具 IDG_VS_MM_TOOLSADDINS
Window/Help/Community IDG_VS_MM_WINDOWHELP
加載項 IDG_VS_MM_MACROS
FullScreenBar IDG_VS_MM_FULLSCREENBAR

比如,如果把"項目(IDG_VS_MM_PROJECT)"作為父級,那么效果可以是:

嗯,你想得美~

  • 若要向已有菜單添加新的組,可將組的父級設置為如下值:
菜單 ID
文件 IDM_VS_MENU_FILE
編輯 IDM_VS_MENU_EDIT
視圖 IDM_VS_MENU_VIEW
重構 IDM_VS_MENU_REFACTORING
項目 IDM_VS_MENU_PROJECT
生成 IDM_VS_MENU_BUILD
分析 IDM_VS_MENU_FORMAT
工具 IDM_VS_MENU_TOOLS
擴展 IDM_VS_MENU_EXTENSIONS
窗口 IDM_VS_MENU_WINDOW
加載項 IDM_VS_MENU_ADDINS
社區 IDM_VS_MENU_COMMUNITY
幫助 IDM_VS_MENU_HELP

比如,如果把"視圖(IDM_VS_MENU_VIEW)"作為父級,那么效果可以是:

image

  • 若要在已有菜單的子菜單中添加組,那么可以將已有菜單的子作為父級:
父組 子組
IDG_VS_FILE_FILE IDM_VS_CSCD_NEW IDG_VS_FILE_NEW_CASCADE
IDM_VS_CSCD_OPEN IDG_VS_FILE_OPENP_CASCADE
IDG_VS_FILE_OPENF_CASCADE
IDG_VS_FILE_ADD IDM_VS_CSCD_ADD IDG_VS_FILE_ADD_PROJECT_NEW
IDG_VS_FILE_ADD_PROJECT_EXI
IDG_VS_FILE_MRU IDM_VS_CSCD_FILEMRU IDG_VS_FILE_FMRU_CASCADE
IDM_VS_CSCD_PROJMRU IDG_VS_FILE_PMRU_CASCADE
IDG_VS_FILE_MOVE IDM_VS_CSCD_MOVETOPRJ IDG_VS_FILE_MOVE_CASCADE
IDG_VS_FILE_MOVE_PICKER
IDG_VS_VIEW_DEV_WINDOWS IDM_VS_CSCD_FINDRESULTS IDG_VS_WNDO_FINDRESULTS
IDM_VS_CSCD_WINDOWS IDG_VS_VIEW_CALLBROWSER
IDG_VS_WNDO_OTRWNDWS1...6
IDG_VS_WNDO_WINDOWS2
IDG_VS_VIEW_TOOLBARS IDM_VS_CSCD_COMMANDBARS
IDG_VS_EDIT_GOTO IDM_VS_EDITOR_FIND_MENU
IDG_VS_EDIT_OBJECTS IDM_VS_CSCD_MNUDES IDG_VS_MNUDES_INSERT
IDG_VS_MNUDES_EDITNAMES
IDM_VS_CSCD_OLEVERBS IDG_VS_EDIT_OLEVERBS
IDG_VS_BUILD_SELECTION IDM_VS_CSCD_BUILD IDG_VS_BUILD_CASCADE
IDG_VS_BUILD_PROJPICKER
IDM_VS_CSCD_REBUILD IDG_VS_REBUILD_CASCADE
IDG_VS_REBUILD_PROJPICKER
IDM_VS_CSCD_PROJONLY IDG_VS_PROJONLY_CASCADE
IDM_VS_CSCD_CLEAN IDG_VS_CLEAN_CASCADE
IDG_VS_CLEAN_PROJPICKER
IDM_VS_CSCD_DEPLOY IDG_VS_DEPLOY_CASCADE
IDG_VS_DEPLOY_PROJPICKER
IDG_VS_PGO_SELECTION IDM_VS_CSCD_PGO_BUILD IDG_VS_PGO_BUILD_CASCADE_BUILD
IDG_VS_PGO_BUILD_CASCADE_RUN

比如,如果把"文件-新建(IDM_VS_CSCD_NEW)"作為父級,那么效果可以是:

image

發布Visual Studio擴展

https://docs.microsoft.com/en-au/visualstudio/extensibility/walkthrough-publishing-a-visual-studio-extension

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM