代码改变世界

分享 Visual Studio 2010 扩展制作的心得:基本功能篇

2011-01-21 11:09  Nana's Lich  阅读(7095)  评论(12编辑  收藏  举报

背景

几个月前我做了一个给 Visual Studio 2010 增加背景图片的扩展,不过说实在的这个东西也是借鉴了日本人做的同类扩展

没想到放出来之后有很多人对此感兴趣,我也因此决定增加配置对话框功能,免去手工编辑配置文件的麻烦。

但是因为一些奇怪的原因,说好的功能一直没有去做,在这期间也有别人在此基础上进行改造(虽然有点无伤大雅的小小误会),受这件事情的刺激我决定完善这个扩展,让它成为一个说得过去的产品。

而在完善这个扩展的过程中也遭遇了种种的困难,为了让对这方面感兴趣的同学们以后少走弯路,我决定以博文为载体将解决这些问题的经验分享出来。

基本功能的实现

我这个扩展到基本功能么,就是给 Visual Studio 2010 里面的文本编辑器增加背景图案,然后还要使用一个 XML 文件来保存这些配置。

原理呢,就是通过编辑器修饰物扩展的实现,来在编辑器中插入 Canvas 控件和 Image 控件(WPF),来绘制背景,效果呢大概就是这样:

 

通过 Visual Studio SDK 所提供的项目模板可以创建简单的编辑器修饰项目,模板中实现的是一个紫色的方块,我们需要把它改造成我们需要的东西。

背景图片

单纯的背景图片实现其实是很容易的事情,因为通过模板创建的项目就是使用 Image 控件来显示紫色方块的,只要设置好其 Source 属性,再设定其位置和一些其它的杂项属性就成了。

设置 Source 属性

因为目标是图片文件,所以设置 Source 属性时的首选类型是 BitmapImage。

不过 BitmapImage 的默认构造器会独占目标图片文件,因而导致程序运行过程中无法修改或移动目标图片文件。

解决这个问题的办法则是使用无参数的构造器,并且手工编写初始化步骤:

var bmp = new BitmapImage();
bmp.BeginInit();
bmp.CreateOptions = BitmapCreateOptions.IgnoreImageCache;
bmp.CacheOption = BitmapCacheOption.OnLoad;
bmp.UriSource = new Uri(value);
bmp.EndInit();

设定位置和大小

项目的默认实现采用的是固定大小的 Image 控件,并将之放在右上角。

而对于我们采用的背景图片来说,可能放在其它位置更好,比方说我最喜欢的一张比较适合放在右下角。

要改变其位置也并不复杂,只要让 Image 控件填满整个空间,然后设置它的 HorizontalAlignment 和 VerticalAlignment 属性就可以了。

这里需要注意的是,ItaBackgroundImage 把 Image 控件的位置坐标设置成了『0, 0』,而当编辑器大小改变(比方说缩放了 Visual Studio 的窗口)时这种设计会导致背景图片错位。

正确的方法应该是这样做:

Canvas.SetLeft(element, textView.ViewportLeft);
Canvas.SetTop(element, textView.ViewportTop);

element.Width = textView.ViewportWidth;
element.Height = textView.ViewportHeight;

adornmentLayer.AddAdornment(AdornmentPositioningBehavior.ViewportRelative,
  null, null, element, null);

墙纸图案

和背景图片不同,墙纸图案通常是重复绘制来填充整个背景空间的,所以使用 Canvas 和 ImageBrush 来配合的效果比较好,ImageBrush 的 TileMode 属性应该设置成 None 以外的项目。

不过使用过我之前发布的 ItaBackgroundKyuu 的同学可能会注意到,ItaBackgroundKyuu 的墙纸图案是“固定位置”的,当文本进行滚动的时候墙纸并不会跟随着一起滚动,这样用起来会感觉很奇怪,因此我又花了些时间来研究让墙纸跟随文本一起滚动的办法。

通过调用 AddAdornment 时指定 AdornmentPositioningBehavior.OwnerControlled 可以初步达到这个目的。

纵向坐标偏移的问题

如果改变了编辑器的显示比例(Zoom),或者从非正常关闭中恢复 Visual Studio 时,编辑器视图的坐标系可能会发生改变;如果这个时候向上滚动编辑器,就会发现前面的部分并没有绘制墙纸图案,甚至还有可能会引发异常。

借助 Reflector 等手段研究了一番之后,并没有发现什么可以确定文本中第一行的纵向坐标的简单办法,不知道是不是搞错了探索的方向。

不过在这个过程中却发现编辑器内部似乎是从某一行开始一行一行地累加行高,所以经过若干小时的尝试之后,最终我也采用了累加行高的办法,这部分是这样实现的:

private double GetFirstLineTop(IWpfTextView view)
{
  var fvLine = view.TextViewLines.FirstVisibleLine;
  double top = fvLine.Top;

  var ss = fvLine.Snapshot;
  int lastIndex = fvLine.Start;

  IWpfTextViewLine line;
  while (lastIndex > 0)
  {
    line = view.GetTextViewLineContainingBufferPosition(new SnapshotPoint(ss, lastIndex - 1));
    top -= line.Height;

    lastIndex = line.Start;
  }

  return top;
}

在 ITextView 的 LayoutChanged 事件中写入下面这样的代码,就可以让墙纸图案随着文本一并滚动了,而且改变显示比例也不会导致错位问题:

var tileTop = GetFirstLineTop(textView);
Canvas.SetTop(tile, tileTop); // tile 是用于呈现墙纸图案的 Canvas 控件

var state = e.NewViewState;
tile.Width = state.ViewportRight;
tile.Height = state.ViewportBottom - tileTop;

修饰层的呈现优先顺序

修饰层以哪种优先级呈现会影响到修饰层中的修饰物件会遮挡住什么、可以被什么遮挡住。

修饰项目默认采用的修饰层是这样设定的:

[Export(typeof(AdornmentLayerDefinition))]
[Name("ViewportAdornment1")]
[Order(After = PredefinedAdornmentLayers.Caret)]
[TextViewRole(PredefinedTextViewRoles.Document)]
public AdornmentLayerDefinition editorAdornmentLayer = null;

然而在实际使用中会发现我们的修饰层可能会意外地显示不正确,调试过程中可以发现在能满足 [Order(After = PredefinedAdornmentLayers.Caret)] 的前提下,我们的修饰层的呈现顺序并不确定,而是随机插入在其它修饰层之间的。

为了让它正确呈现,我们需要指定一个较为严格的顺序,比方说:

[Order(Before = PredefinedAdornmentLayers.Outlining)]

这样我们的修饰物就总会出现在编辑器的最底层了。

设置的存储

为了省却一部分人在重装系统之后可能需要重新设置的麻烦,我把设置以 XML 文件的形式保存在了『我的文档』之中。

由于采用的是独立的 XML 文件,因此直接使用了 XML 序列化功能,而没有去使用 .NET 框架所推荐的 ConfigurationSection 设计。

这部分没有什么需要特别注意的技术细节,也不作过多的说明了。

后记

这一次主要说的是实现基本功能时需要注意的技术细节,然而在完善它使其成为一款真正的『产品』的过程中还有很多其它方面的问题,这些问题以及解决的办法我也会在以后的博文中一一列举出来。

博文大致会写成这样的几篇:

  • 设置对话框的实现;
  • 打包 VSIX 的心得;
  • 编程过程中遭遇的陷阱。

最后,附上我的扩展在『Visual Studio 扩展库』上的下载页面链接:

http://visualstudiogallery.msdn.microsoft.com/zh-cn/2d4f653f-262a-4d3a-b42c-c6f8e35e777e

发布在扩展库上就意味着安装扩展的用户以后都能及时收到更新。

需要注意的是,扩展名称已经从『ItaBackgroundKyuu』改成了『KyuuBackground』,名称上的改变导致之前版本的配置文件也不再起作用了,需要通过选项对话框重新设置。