Here is some code that uses the .Net Automation library to copy all the text to the clipboard.
Start a new WinForms
project and then add the following references:
- WindowsBase
- UIAutomationTypes
- UIAutomationClient
- System.Xaml
- PresentationCore
- PresentationFramework
- System.Management
The code also explains how to setup a menu item in visual studio to copy the contents to the clipboard.
Edit: The UI Automation only returns visible tree view items. Thus, to copy all the items, the find symbol results window is set as foreground, and then a {PGDN}
is sent, and the next batch of items is copied. This process is repeated until no new items are found. It would have been preferable to use the ScrollPattern
, however it threw an Exception
when trying to set the scroll.
Edit 2: Tried to improve the performance of AutomationElement FindAll by running on a separate thread. Seems to be slow in some cases.
Edit 3: Improved performance by making the TreeView window very large. Can copy about 400 items in about 10 seconds.
Edit 4: Dispose objects implementing IDisposable
. Better message reporting. Better handling of process args. Put window back to its original size.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Management;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Windows.Automation;
using System.Windows.Forms;
namespace CopyFindSymbolResults {
// This program tries to find the 'Find Symbol Results' window in visual studio
// and copy all the text to the clipboard.
//
// The Find Symbol Results window uses a TreeView control that has the class name 'LiteTreeView32'
// In the future if this changes, then it's possible to pass in the class name as the first argument.
// Use TOOLS -> Spy++ to determine the class name.
//
// After compiling this code into an Exe, add a menu item (TOOLS -> Copy Find Symbol Results) in Visual Studio by:
// 1) TOOLS -> External Tools...
// (Note: in the 'Menu contents:' list, count which item the new item is, starting at base-1).
// Title: Copy Find Symbol Results
// Command: C:\<Path>\CopyFindSymbolResults.exe (e.g. C:\Windows\ is one option)
// 2) TOOLS -> Customize... -> Keyboard... (button)
// Show Commands Containing: tools.externalcommand
// Then select the n'th one, where n is the count from step 1).
//
static class Program {
enum Tabify {
No = 0,
Yes = 1,
Prompt = 2,
}
[STAThread]
static void Main(String[] args) {
String className = "LiteTreeView32";
Tabify tabify = Tabify.Prompt;
if (args.Length > 0) {
String arg0 = args[0].Trim();
if (arg0.Length > 0)
className = arg0;
if (args.Length > 1) {
int x = 0;
if (int.TryParse(args[1], out x))
tabify = (Tabify) x;
}
}
DateTime startTime = DateTime.Now;
Data data = new Data() { className = className };
Thread t = new Thread((o) => {
GetText((Data) o);
});
t.IsBackground = true;
t.Start(data);
lock(data) {
Monitor.Wait(data);
}
if (data.p == null || data.p.MainWindowHandle == IntPtr.Zero) {
System.Windows.Forms.MessageBox.Show("Cannot find Microsoft Visual Studio process.");
return;
}
try {
SimpleWindow owner = new SimpleWindow { Handle = data.MainWindowHandle };
if (data.appRoot == null) {
System.Windows.Forms.MessageBox.Show(owner, "Cannot find AutomationElement from process MainWindowHandle: " + data.MainWindowHandle);
return;
}
if (data.treeViewNotFound) {
System.Windows.Forms.MessageBox.Show(owner, "AutomationElement cannot find the tree view window with class name: " + data.className);
return;
}
String text = data.text;
if (text.Length == 0) { // otherwise Clipboard.SetText throws exception
System.Windows.Forms.MessageBox.Show(owner, "No text was found: " + data.p.MainWindowTitle);
return;
}
TimeSpan ts = DateTime.Now - startTime;
if (tabify == Tabify.Prompt) {
var dr = System.Windows.Forms.MessageBox.Show(owner, "Replace dashes and colons for easy pasting into Excel?", "Tabify", System.Windows.Forms.MessageBoxButtons.YesNo);
if (dr == System.Windows.Forms.DialogResult.Yes)
tabify = Tabify.Yes;
ts = TimeSpan.Zero; // prevent second prompt
}
if (tabify == Tabify.Yes) {
text = text.Replace(" - ", "\t");
text = text.Replace(" : ", "\t");
}
System.Windows.Forms.Clipboard.SetText(text);
String msg = "Data is ready on the clipboard.";
var icon = System.Windows.Forms.MessageBoxIcon.None;
if (data.lines != data.count) {
msg = String.Format("Only {0} of {1} rows copied.", data.lines, data.count);
icon = System.Windows.Forms.MessageBoxIcon.Error;
}
if (ts.TotalSeconds > 4 || data.lines != data.count)
System.Windows.Forms.MessageBox.Show(owner, msg, "", System.Windows.Forms.MessageBoxButtons.OK, icon);
} finally {
data.p.Dispose();
}
}
private class SimpleWindow : System.Windows.Forms.IWin32Window {
public IntPtr Handle { get; set; }
}
private const int TVM_GETCOUNT = 0x1100 + 5;
[DllImport("user32.dll")]
static extern int SendMessage(IntPtr hWnd, int msg, int wparam, int lparam);
[DllImport("user32.dll", SetLastError = true)]
static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int Width, int Height, bool Repaint);
private class Data {
public int lines = 0;
public int count = 0;
public IntPtr MainWindowHandle = IntPtr.Zero;
public IntPtr TreeViewHandle = IntPtr.Zero;
public Process p;
public AutomationElement appRoot = null;
public String text = null;
public String className = null;
public bool treeViewNotFound = false;
}
private static void GetText(Data data) {
Process p = GetParentProcess();
data.p = p;
if (p == null || p.MainWindowHandle == IntPtr.Zero) {
data.text = "";
lock(data) { Monitor.Pulse(data); }
return;
}
data.MainWindowHandle = p.MainWindowHandle;
AutomationElement appRoot = AutomationElement.FromHandle(p.MainWindowHandle);
data.appRoot = appRoot;
if (appRoot == null) {
data.text = "";
lock(data) { Monitor.Pulse(data); }
return;
}
AutomationElement treeView = appRoot.FindFirst(TreeScope.Subtree, new PropertyCondition(AutomationElement.ClassNameProperty, data.className));
if (treeView == null) {
data.text = "";
data.treeViewNotFound = true;
lock(data) { Monitor.Pulse(data); }
return;
}
data.TreeViewHandle = new IntPtr(treeView.Current.NativeWindowHandle);
data.count = SendMessage(data.TreeViewHandle, TVM_GETCOUNT, 0, 0);
RECT rect = new RECT();
GetWindowRect(data.TreeViewHandle, out rect);
// making the window really large makes it so less calls to FindAll are required
MoveWindow(data.TreeViewHandle, 0, 0, 800, 32767, false);
int TV_FIRST = 0x1100;
int TVM_SELECTITEM = (TV_FIRST + 11);
int TVGN_CARET = TVGN_CARET = 0x9;
// if a vertical scrollbar is detected, then scroll to the top sending a TVM_SELECTITEM command
var vbar = treeView.FindFirst(TreeScope.Subtree, new PropertyCondition(AutomationElement.NameProperty, "Vertical Scroll Bar"));
if (vbar != null) {
SendMessage(data.TreeViewHandle, TVM_SELECTITEM, TVGN_CARET, 0); // select the first item
}
StringBuilder sb = new StringBuilder();
Hashtable ht = new Hashtable();
int chunk = 0;
while (true) {
bool foundNew = false;
AutomationElementCollection treeViewItems = treeView.FindAll(TreeScope.Subtree, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.TreeItem));
if (treeViewItems.Count == 0)
break;
if (ht.Count == 0) {
chunk = treeViewItems.Count - 1;
}
foreach (AutomationElement ele in treeViewItems) {
try {
String n = ele.Current.Name;
if (!ht.ContainsKey(n)) {
ht[n] = n;
foundNew = true;
data.lines++;
sb.AppendLine(n);
}
} catch {}
}
if (!foundNew || data.lines == data.count)
break;
int x = Math.Min(data.count-1, data.lines + chunk);
SendMessage(data.TreeViewHandle, TVM_SELECTITEM, TVGN_CARET, x);
}
data.text = sb.ToString();
MoveWindow(data.TreeViewHandle, rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top, false);
lock(data) { Monitor.Pulse(data); }
}
// this program expects to be launched from Visual Studio
// alternative approach is to look for "Microsoft Visual Studio" in main window title
// but there could be multiple instances running.
private static Process GetParentProcess() {
// from thread: http://stackoverflow.com/questions/2531837/how-can-i-get-the-pid-of-the-parent-process-of-my-application
int myId = 0;
using (Process current = Process.GetCurrentProcess())
myId = current.Id;
String query = String.Format("SELECT ParentProcessId FROM Win32_Process WHERE ProcessId = {0}", myId);
using (var search = new ManagementObjectSearcher("root\\CIMV2", query)) {
using (ManagementObjectCollection list = search.Get()) {
using (ManagementObjectCollection.ManagementObjectEnumerator results = list.GetEnumerator()) {
if (!results.MoveNext()) return null;
using (var queryObj = results.Current) {
uint parentId = (uint) queryObj["ParentProcessId"];
return Process.GetProcessById((int) parentId);
}
}
}
}
}
[DllImport("user32.dll")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[StructLayout(LayoutKind.Sequential)]
private struct RECT {
public int Left;
public int Top;
public int Right;
public int Bottom;
}
}
}