2025-11-28 00:35:46 +09:00

1241 lines
42 KiB
C++

// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// Copyright (c) Microsoft Corporation. All rights reserved
#include <windows.h>
#include <ole2.h>
#include <uiautomation.h>
#include <strsafe.h>
IUIAutomation *_automation;
// Maximum Text length we will read
const int maxLength = 10000;
// Maximum number of comments we'll search for
const int maxComments = 25;
// a class used to wrap and access a Comment Element
class Comment
{
public:
// This takes ownership and responsibility for releasing the element reference
// So it does not AddRef it, the caller should not Release the element after
// it has created the Comment object, as it has passed ownership to it.
Comment(_In_ IUIAutomationElement *element) : _commentElement(element)
{
_annotation = NULL;
}
~Comment()
{
_commentElement->Release();
if (_annotation != NULL)
{
_annotation->Release();
}
}
bool Compare(_In_opt_ Comment *other)
{
bool retVal = false;
if (other != NULL)
{
BOOL compResult;
HRESULT hr = _automation->CompareElements(_commentElement, other->_commentElement, &compResult);
if (FAILED(hr))
{
wprintf(L"CompareElements failed, HR: 0x%08x\n", hr);
}
else
{
retVal = (compResult == TRUE);
}
}
return retVal;
}
// Should be immediately after constructor, other calls rely on _annotation being set
HRESULT GetAnnotationPattern()
{
HRESULT hr = _commentElement->GetCurrentPatternAs(UIA_AnnotationPatternId, IID_PPV_ARGS(&_annotation));
if (FAILED(hr))
{
wprintf(L"Failed to Get Annotation Pattern, HR: 0x%08x\n", hr);
}
else if (_annotation == NULL)
{
wprintf(L"Element does not actually support Annotation Pattern\n");
hr = E_FAIL;
}
return hr;
}
HRESULT RangeFromAnnotation(_In_ IUIAutomationTextPattern2 *textPattern, _Outptr_result_maybenull_ IUIAutomationTextRange **range)
{
HRESULT hr = textPattern->RangeFromAnnotation(_commentElement, range);
if (FAILED(hr))
{
wprintf(L"RangeFromAnnotation failed, HR: 0x%08x\n", hr);
}
return hr;
}
void Print(_In_ bool summary)
{
BSTR name;
HRESULT hr = _commentElement->get_CurrentName(&name);
if (FAILED(hr))
{
wprintf(L"Failed to Get Name Property, HR: 0x%08x\n", hr);
}
else
{
if (summary)
{
wprintf(L"\"%30s\"\n", name);
}
else
{
BSTR typeName;
hr = _annotation->get_CurrentAnnotationTypeName(&typeName);
if (FAILED(hr))
{
wprintf(L"Failed to Get AnnotationTypeName Property, HR: 0x%08x\n", hr);
}
else
{
BSTR author;
hr = _annotation->get_CurrentAuthor(&author);
if (FAILED(hr))
{
wprintf(L"Failed to Get Author Property, HR: 0x%08x\n", hr);
}
else
{
BSTR dateTime;
hr = _annotation->get_CurrentDateTime(&dateTime);
if (FAILED(hr))
{
wprintf(L"Failed to Get DateTime Property, HR: 0x%08x\n", hr);
}
else
{
wprintf(L"Type: %s\nAuthor: %s\nDate/Time: %s\n%s\n", typeName, author, dateTime, name);
SysFreeString(dateTime);
}
SysFreeString(author);
}
SysFreeString(typeName);
}
}
SysFreeString(name);
}
}
private:
IUIAutomationElement* _commentElement;
IUIAutomationAnnotationPattern* _annotation;
};
// A class used to wrap and access a Text Range
class Range
{
public:
// This takes ownership and responsibility for releasing the range reference
// So it does not AddRef it, the caller should not Release the range after
// it has created the range object, as it has passed ownership to it.
Range(_In_opt_ IUIAutomationTextRange *range)
{
_range = range;
}
~Range()
{
if (_range != NULL)
{
_range->Release();
}
}
// Assumes that _range is NOT null
void FindAndPrintStyle(_In_ int style)
{
IUIAutomationTextRange *remainingRange;
HRESULT hr = _range->Clone(&remainingRange);
if (FAILED(hr))
{
wprintf(L"Clone failed on range, HR: 0x%08x\n", hr);
}
else
{
VARIANT styleVar;
styleVar.vt = VT_I4;
styleVar.lVal = style;
while (SUCCEEDED(hr))
{
IUIAutomationTextRange *foundRange;
hr = remainingRange->FindAttribute(UIA_StyleIdAttributeId, styleVar, FALSE, &foundRange);
if (FAILED(hr))
{
wprintf(L"FindAttribute failed, HR: 0x%08x\n", hr);
}
else
{
if (foundRange != NULL)
{
BSTR text;
hr = foundRange->GetText(maxLength, &text);
if (FAILED(hr))
{
wprintf(L"GetText failed, HR: 0x%08x\n", hr);
}
else
{
wprintf(L" - \"%s\"\n", text);
SysFreeString(text);
}
// Next, to continue the loop, we move the beginning endpoint of our working range (remainingRange), forward
// to the ending point of the last range we found. This gives us a range containing all the text we haven't
// searched yet.
hr = remainingRange->MoveEndpointByRange(TextPatternRangeEndpoint_Start, foundRange, TextPatternRangeEndpoint_End);
foundRange->Release();
if (FAILED(hr))
{
wprintf(L"MoveEndpointByRange failed, HR: 0x%08x\n", hr);
}
}
else
{
break;
}
}
}
remainingRange->Release();
}
}
void FindAndPrintHeaders()
{
if (_range == NULL)
{
wprintf(L"Headers: <None>\n");
}
else
{
wprintf(L"Title:\n");
FindAndPrintStyle(StyleId_Title);
wprintf(L"Heading 1:\n");
FindAndPrintStyle(StyleId_Heading1);
wprintf(L"Heading 2:\n");
FindAndPrintStyle(StyleId_Heading2);
wprintf(L"Heading 3:\n");
FindAndPrintStyle(StyleId_Heading3);
}
}
// This is a quick check of whether a specific range (the input), is past the endpoint
// of this range object (the internal _range of this Range object). It does this by
// checking whether the start point of the input range is greater than or equal to the
// endpoint of the internal range.
bool IsThisRangePastMyEndpoint(_In_ IUIAutomationTextRange *range)
{
bool retVal = true;
int compare;
HRESULT hr = _range->CompareEndpoints(TextPatternRangeEndpoint_End, range, TextPatternRangeEndpoint_Start, &compare);
if (FAILED(hr))
{
// If this call fails, assume we're past the end
wprintf(L"CompareEndpoints failed on range, HR: 0x%08x\n", hr);
}
else
{
retVal = compare <= 0;
}
return retVal;
}
// This will extract the comments from an IUIAutomationElementArray, adding any new ones to the comments array
HRESULT GetNewCommentsFromArray(_In_ IUIAutomationElementArray *commentElements, _Inout_updates_to_(commentCount, *foundCommentCount) Comment **comments,
_In_ int commentCount, _Inout_ int *foundCommentCount)
{
HRESULT hr = S_OK;
int count;
hr = commentElements->get_Length(&count);
if (FAILED(hr))
{
wprintf(L"get_Length failed on element array, HR: 0x%08x\n", hr);
}
else
{
for (int i = 0; i < count && SUCCEEDED(hr); i++)
{
IUIAutomationElement *element;
hr = commentElements->GetElement(i, &element);
if (FAILED(hr) || element == NULL)
{
wprintf(L"GetElement failed on element array, HR: 0x%08x\n", hr);
}
else
{
bool addedComment = false;
Comment *newComment = new Comment(element);
if (newComment == NULL)
{
wprintf(L"Not enough memory to create a new Comment\n");
// Comment failed to be created, so did not take ownership of element
element->Release();
hr = E_OUTOFMEMORY;
}
else
{
hr = newComment->GetAnnotationPattern();
if (SUCCEEDED(hr))
{
bool found = false;
// check if the comment is already in the array
for (int j = 0; j < *foundCommentCount; j++)
{
if (newComment->Compare(comments[j]))
{
found = true;
break;
}
}
// If it's not there, add it to the list
if (!found)
{
if ((*foundCommentCount) < commentCount)
{
// The array takes ownership of the comment at this point
comments[*foundCommentCount] = newComment;
addedComment = true;
(*foundCommentCount)++;
}
}
}
if (!addedComment)
{
delete newComment;
}
}
}
_Analysis_assume_((*foundCommentCount) <= commentCount);
}
}
return hr;
}
// This call will move the range argument, so don't pass in a range you don't want moved, clone it first
HRESULT WalkForCommentsInRange(_In_ IUIAutomationTextRange *range, _Inout_updates_to_(commentCount, *foundCommentCount) Comment **comments,
_In_ int commentCount, _Inout_ int *foundCommentCount)
{
HRESULT hr = S_OK;
// These are singleton objects so don't need to be released
IUnknown *notSupported = NULL;
IUnknown *mixedAttribute = NULL;
hr = _automation->get_ReservedNotSupportedValue(&notSupported);
if (FAILED(hr))
{
wprintf(L"get_ReservedNotSupportedValue failed, HR: 0x%08x\n", hr);
}
else
{
hr = _automation->get_ReservedMixedAttributeValue(&mixedAttribute);
if (FAILED(hr))
{
wprintf(L"get_ReservedMixedAttributeValue failed, HR: 0x%08x\n", hr);
}
}
// A way to prevent this from running into a loop if a provider doesn't implement Move properly
int sanityCount = 100;
while (!IsThisRangePastMyEndpoint(range) && SUCCEEDED(hr) && sanityCount-- > 0)
{
// Get the list of comments on the current range
VARIANT varComments;
hr = range->GetAttributeValue(UIA_AnnotationObjectsAttributeId, &varComments);
if (FAILED(hr))
{
wprintf(L"GetAttributeValue failed on range, HR: 0x%08x\n", hr);
}
else
{
if (varComments.vt != VT_EMPTY && varComments.vt != VT_UNKNOWN )
{
wprintf(L"Unexpected Type for AnnotationObjectsAttribute, vt: 0x%08x\n", varComments.vt);
hr = E_FAIL;
}
else if (varComments.vt == VT_UNKNOWN)
{
if (varComments.punkVal == notSupported)
{
wprintf(L"AnnotationObjectsAttribute not supported on this Range\n");
hr = E_FAIL;
}
else if (varComments.punkVal == mixedAttribute)
{
// Since we're walking by format, mixed Attribute value should never come up
wprintf(L"Unexpected Mixed Attribute Value\n");
hr = E_FAIL;
}
else
{
IUIAutomationElementArray *commentElements;
hr = varComments.punkVal->QueryInterface(IID_PPV_ARGS(&commentElements));
if (FAILED(hr))
{
wprintf(L"Could not QI to IUIAutomationElementArray, HR: 0x%08x\n", hr);
}
else
{
hr = GetNewCommentsFromArray(commentElements, comments, commentCount, foundCommentCount);
commentElements->Release();
}
}
}
VariantClear(&varComments);
}
// If nothing has gone wrong, advance the range
if (SUCCEEDED(hr))
{
int moveCount;
hr = range->Move(TextUnit_Format, 1, &moveCount);
if (FAILED(hr))
{
wprintf(L"Move by format failed on range, HR: 0x%08x\n", hr);
}
else if (moveCount != 1)
{
wprintf(L"Failed to advance forward for some reason, terminating loop\n");
hr = E_FAIL;
}
}
}
return hr;
}
HRESULT FindCommentsInRange(_Out_writes_to_(commentCount, *foundCommentCount) Comment **comments, _In_ int commentCount, _Out_ int *foundCommentCount)
{
*foundCommentCount = 0;
IUIAutomationTextRange *movingRange;
HRESULT hr = _range->Clone(&movingRange);
if (FAILED(hr))
{
wprintf(L"Clone failed on range, HR: 0x%08x\n", hr);
}
else
{
// First collapse the range to the starting endpoint by moving the end endpoint back by the whole document
hr = movingRange->MoveEndpointByRange(TextPatternRangeEndpoint_End, movingRange, TextPatternRangeEndpoint_Start);
if (FAILED(hr))
{
wprintf(L"MoveEndpointByUnit failed on range, HR: 0x%08x\n", hr);
}
else
{
hr = movingRange->ExpandToEnclosingUnit(TextUnit_Format);
if (FAILED(hr))
{
wprintf(L"ExpandToEnclosingUnit failed on range, HR: 0x%08x\n", hr);
}
else
{
// Now walk through in Format units, getting the annotations
hr = WalkForCommentsInRange(movingRange, comments, commentCount, foundCommentCount);
}
}
movingRange->Release();
}
return hr;
}
void FindAndPrintComments()
{
if (_range == NULL)
{
wprintf(L"Comments: <None>\n");
}
else
{
wprintf(L"Comments:\n");
Comment **comments = new Comment*[maxComments];
if (comments == NULL)
{
wprintf(L"Not enough memory to create a Comment Array\n");
}
else
{
int foundComments;
HRESULT hr = FindCommentsInRange(comments, maxComments, &foundComments);
if (SUCCEEDED(hr))
{
for (int i = 0; i < foundComments; i++)
{
wprintf(L"%2d: ", i);
comments[i]->Print(true);
}
for (int i = 0; i < foundComments; i++)
{
delete comments[i];
}
}
delete [] comments;
}
}
}
void SelectComment(_In_ int commentNum, _Outptr_result_maybenull_ Comment **comment)
{
*comment = NULL;
if (_range != NULL)
{
Comment **comments = new Comment*[maxComments];
if (comments == NULL)
{
wprintf(L"Not enough memory to create a Comment Array\n");
}
else
{
int foundComments;
HRESULT hr = FindCommentsInRange(comments, maxComments, &foundComments);
if (SUCCEEDED(hr))
{
if (commentNum >= 0 && commentNum < foundComments)
{
*comment = comments[commentNum];
comments[commentNum] = NULL;
}
for (int i = 0; i < foundComments; i++)
{
if (i != commentNum)
{
delete comments[i];
}
}
}
delete [] comments;
}
}
}
void PrintCaretInfo()
{
if (_range == NULL)
{
wprintf(L"No Caret Range\n");
}
else
{
VARIANT var;
HRESULT hr = _range->GetAttributeValue(UIA_IsActiveAttributeId, &var);
if (FAILED(hr))
{
wprintf(L"GetAttributeValue on UIA_IsActiveAttributeId failed, HR: 0x%08x\n", hr);
}
else
{
if (var.vt != VT_BOOL)
{
wprintf(L"Unexpected variant type for UIA_IsActiveAttributeId, vt: 0x%08x\n", var.vt);
}
else
{
if (var.boolVal == VARIANT_TRUE)
{
wprintf(L"Active\n");
}
else
{
wprintf(L"Inactive\n");
}
}
}
hr = _range->GetAttributeValue(UIA_CaretPositionAttributeId, &var);
if (FAILED(hr))
{
wprintf(L"GetAttributeValue on UIA_CaretPositionAttributeId failed, HR: 0x%08x\n", hr);
}
else
{
if (var.vt == VT_UNKNOWN)
{
wprintf(L"This provider does not support UIA_CaretPositionAttributeId\n");
}
else if (var.vt != VT_I4)
{
wprintf(L"Unexpected variant type for UIA_CaretPositionAttributeId, vt: 0x%08x\n", var.vt);
}
else
{
// The caret Position attribute is only guaranteed to be accurate when the caret is at
// the beginning or end of a line... otherwise, it is indeterminate.
if (var.lVal == CaretPosition_EndOfLine)
{
wprintf(L"Position is at the end of a line\n");
}
else if (var.lVal == CaretPosition_BeginningOfLine)
{
wprintf(L"Position is at the beginning of a line\n");
}
else if (var.lVal == CaretPosition_Unknown)
{
wprintf(L"Position type is unknown (likely within a line)\n");
}
else
{
wprintf(L"Position type is not a known enum value: 0x%08x\n", var.lVal);
}
}
}
hr = _range->GetAttributeValue(UIA_CaretBidiModeAttributeId, &var);
if (FAILED(hr))
{
wprintf(L"GetAttributeValue on UIA_CaretBidiModeAttributeId failed, HR: 0x%08x\n", hr);
}
else
{
if (var.vt == VT_UNKNOWN)
{
wprintf(L"This provider does not support UIA_CaretBidiModeAttributeId\n");
}
else if (var.vt != VT_I4)
{
wprintf(L"Unexpected variant type for UIA_CaretBidiModeAttributeId, vt: 0x%08x\n", var.vt);
}
else
{
if (var.lVal == CaretBidiMode_LTR)
{
wprintf(L"Text at caret is Left-to-Right reading\n");
}
else if (var.lVal == CaretBidiMode_RTL)
{
wprintf(L"Text at caret is Right-to-Left reading\n");
}
else
{
wprintf(L"BiDi Mode type is not a known enum value: 0x%08x\n", var.lVal);
}
}
}
hr = _range->GetAttributeValue(UIA_SelectionActiveEndAttributeId, &var);
if (FAILED(hr))
{
wprintf(L"GetAttributeValue on UIA_SelectionActiveEndAttributeId failed, HR: 0x%08x\n", hr);
}
else
{
if (var.vt == VT_UNKNOWN)
{
wprintf(L"This provider does not support UIA_SelectionActiveEndAttributeId\n");
}
else if (var.vt != VT_I4)
{
wprintf(L"Unexpected variant type for UIA_SelectionActiveEndAttributeId, vt: 0x%08x\n", var.vt);
}
else
{
if (var.lVal == ActiveEnd_None)
{
wprintf(L"The selection has no active end\n");
}
else if (var.lVal == ActiveEnd_Start)
{
wprintf(L"Active end of selection is the start of the selection\n");
}
else if (var.lVal == CaretBidiMode_RTL)
{
wprintf(L"Active end of selection is the end of the selection\n");
}
else
{
wprintf(L"Active end value is not a known enum value: 0x%08x\n", var.lVal);
}
}
}
SAFEARRAY *psa = NULL;
hr = _range->GetBoundingRectangles(&psa);
if (FAILED(hr))
{
wprintf(L"GetBoundingRectangles on caret range failed, HR: 0x%08x\n", hr);
}
else
{
if (psa != NULL)
{
RECT *pRectArray = NULL;
int cRectCount = 0;
hr = _automation->SafeArrayToRectNativeArray(psa, &pRectArray, &cRectCount);
if (FAILED(hr))
{
wprintf(L"SafeArrayToRectNativeArray on caret range rects failed, HR: 0x%08x\n", hr);
}
else if (pRectArray == NULL)
{
wprintf(L"SafeArrayToRectNativeArray returned a NULL rect array");
}
else
{
// The caret range should never have more than one bounding rect.
if (cRectCount > 1)
{
wprintf(L"Caret range has an unexpected count of bounding rects, count: %d\n", cRectCount);
}
else if (cRectCount == 0)
{
wprintf(L"Caret range has no bounding rect\n");
}
else
{
wprintf(L"Caret range bounding rect is left: %d, top: %d, right: %d, bottom: %d\n",
pRectArray[0].left, pRectArray[0].top, pRectArray[0].right, pRectArray[0].bottom);
}
::CoTaskMemFree(pRectArray);
}
SafeArrayDestroy(psa);
}
else
{
wprintf(L"GetBoundingRectangles on caret range returned no rects.\n");
}
}
}
}
void Print(_In_ bool summary)
{
if (_range == NULL)
{
if (summary)
{
wprintf(L"Active Range: Empty [0 characters]\n");
}
}
else
{
BSTR text;
HRESULT hr = _range->GetText(maxLength, &text);
if (FAILED(hr))
{
wprintf(L"GetText failed, HR: 0x%08x\n", hr);
}
else
{
if (summary)
{
UINT length = SysStringLen(text);
WCHAR shortString[40];
hr = StringCchCopy(shortString, ARRAYSIZE(shortString), text);
if (hr == STRSAFE_E_INSUFFICIENT_BUFFER)
{
WCHAR tail[] = L"...";
int pos = ARRAYSIZE(shortString) - ARRAYSIZE(tail);
hr = StringCchCopy(&shortString[pos], ARRAYSIZE(tail), tail);
}
if (SUCCEEDED(hr))
{
wprintf(L"Active Range: \"%s\" [%d characters]\n", shortString, length);
}
}
else
{
wprintf(L"Active Range:\n%s\n", text);
}
SysFreeString(text);
}
}
}
private:
IUIAutomationTextRange *_range;
};
enum CommandId {
CommandId_Invalid = -1,
CommandId_Help = 0,
CommandId_Print = 1,
CommandId_Document = 2,
CommandId_Selection = 3,
CommandId_Visible = 4,
CommandId_Headers = 5,
CommandId_Comment = 6,
CommandId_Comments = 7,
CommandId_CommentRange=8,
CommandId_PrintComment=9,
CommandId_Exit = 10,
CommandId_Caret = 11
};
PCWSTR CommandText[] = {
L"help\n",
L"print\n",
L"document\n",
L"selection\n",
L"visible\n",
L"headers\n",
L"comment ",
L"comments\n",
L"commentrange\n",
L"printcomment\n",
L"exit\n",
L"caret\n"
};
class TextPatternExplorer
{
public:
// This takes ownership and responsibility for releasing the element reference
// So it does not AddRef it, the caller should not Release the element after
// it has created the Comment object, as it has passed ownership to it.
TextPatternExplorer(_In_ IUIAutomationElement *element) : _element(element)
{
_textPattern = NULL;
}
~TextPatternExplorer()
{
_element->Release();
if (_textPattern != NULL)
{
_textPattern->Release();
}
}
void Run()
{
HRESULT hr = GetTextPattern();
if (SUCCEEDED(hr))
{
Welcome();
Range *currentRange = GetRange(CommandId_Document, NULL);
Comment *currentComment = NULL;
while (currentRange != NULL)
{
currentRange->Print(true);
if (currentComment != NULL)
{
wprintf(L"Active Comment:");
currentComment->Print(true);
}
WCHAR input[40];
wprintf(L">");
fgetws(input, ARRAYSIZE(input), stdin);
CommandId cmdId = GetCommand(input, ARRAYSIZE(input));
switch(cmdId)
{
case CommandId_Help:
{
Welcome();
break;
}
case CommandId_Print:
{
currentRange->Print(false);
break;
}
case CommandId_Document:
case CommandId_Selection:
case CommandId_Visible:
{
delete currentRange;
currentRange = GetRange(cmdId, NULL);
break;
}
case CommandId_Caret:
{
wprintf(L"Pausing for 2 seconds to allow you to focus the text control if desired...\n");
Sleep(2000);
delete currentRange;
bool isActive;
currentRange = GetRange(cmdId, &isActive);
if (currentRange != NULL)
{
wprintf(L"Got Caret Range (%s):\n", isActive ? L"active" : L"inactive" );
currentRange->PrintCaretInfo();
}
break;
}
case CommandId_Headers:
{
currentRange->FindAndPrintHeaders();
break;
}
case CommandId_Comment:
{
SelectComment(input, currentRange, &currentComment);
break;
}
case CommandId_Comments:
{
currentRange->FindAndPrintComments();
break;
}
case CommandId_CommentRange:
{
RangeFromComment(currentComment, &currentRange);
break;
}
case CommandId_PrintComment:
{
if (currentComment != NULL)
{
currentComment->Print(false);
}
break;
}
case CommandId_Exit:
{
delete currentRange;
currentRange = NULL;
break;
}
default:
wprintf(L"Invalid command, type help to see a list of valid commands.\n");
}
}
}
}
private:
void SelectComment(_In_ PWSTR cmdString, _In_ Range *range, _Inout_ Comment **comment )
{
if (*comment != NULL)
{
delete *comment;
*comment = NULL;
}
int commentNumber;
int retVal = swscanf_s(cmdString, L"comment %d", &commentNumber);
if (retVal == 1)
{
range->SelectComment(commentNumber, comment);
}
}
void RangeFromComment(_In_opt_ Comment *comment, _Inout_ Range **range)
{
Range *newRange= NULL;
if (comment == NULL)
{
newRange = new Range(NULL);
if (newRange == NULL)
{
wprintf(L"Not enough memory to create a new Range object\n");
}
}
else
{
IUIAutomationTextRange *textRange;
HRESULT hr = comment->RangeFromAnnotation(_textPattern, &textRange);
if (SUCCEEDED(hr))
{
// We can validly get a NULL range, if the comment points to nothing
newRange = new Range(textRange);
if (newRange == NULL)
{
wprintf(L"Failed to create internal Range object\n");
if (textRange != NULL)
{
textRange->Release();
}
}
}
}
// Any failure to create a new Range object will result in the old range being kept
if (newRange != NULL)
{
delete *range;
*range = newRange;
}
}
CommandId GetCommand(_In_reads_(cmdLength) PWSTR cmd, _In_ int cmdLength)
{
_wcslwr_s(cmd, cmdLength);
CommandId ret = CommandId_Invalid;
for (int i = 0; i < ARRAYSIZE(CommandText); i++)
{
size_t commandLength;
HRESULT hr = StringCchLength(CommandText[i], cmdLength, &commandLength);
// Compare the strings, and the terminating null
if (SUCCEEDED(hr))
{
if (wcsncmp(CommandText[i], cmd, commandLength) == 0)
{
ret = static_cast<CommandId>(i);
break;
}
}
}
return ret;
}
void Welcome()
{
wprintf(L"Text Document Explorer:\n\n");
wprintf(L"Available Commands:\n");
wprintf(L" help Show this help screen\n");
wprintf(L" print Print the text of the current active range\n");
wprintf(L" document Set the active range to the whole document\n");
wprintf(L" selection Set the active range to the current selection\n");
wprintf(L" visible Set the active range to the currently visible text\n");
wprintf(L" headers Find all the header text in the active range and print it\n");
wprintf(L" comments List all the comments in the active range\n");
wprintf(L" comment {n} Set the active comment to comment {n} in the active range\n");
wprintf(L" commentrange Set the active range to the the active comment's range\n");
wprintf(L" printcomment Print the active comment\n");
wprintf(L" caret Gets the caret range and prints some caret specific information\n");
wprintf(L" exit Quit\n");
}
Range* GetRange(_In_ CommandId cmd, _Out_opt_ bool *active)
{
Range* retVal = NULL;
if (active != NULL)
{
*active = false;
}
IUIAutomationTextRange *uiaRange = NULL;
HRESULT hr = E_FAIL;
if (cmd == CommandId_Document)
{
hr = _textPattern->get_DocumentRange(&uiaRange);
}
else if (cmd == CommandId_Caret)
{
BOOL isActive;
hr = _textPattern->GetCaretRange(&isActive, &uiaRange);
if (SUCCEEDED(hr) && active != NULL)
{
*active = !!isActive;
}
}
else
{
// These two properties return arrays of ranges, for complicated text patterns
// for this example we just get the first range from the array
IUIAutomationTextRangeArray *uiaRangeArray = NULL;
if (cmd == CommandId_Selection)
{
hr = _textPattern->GetSelection(&uiaRangeArray);
}
else if (cmd == CommandId_Visible)
{
hr = _textPattern->GetVisibleRanges(&uiaRangeArray);
}
if (SUCCEEDED(hr) && uiaRangeArray != NULL)
{
int length;
hr = uiaRangeArray->get_Length(&length);
if (SUCCEEDED(hr) && length > 0)
{
hr = uiaRangeArray->GetElement(0, &uiaRange);
}
uiaRangeArray->Release();
}
}
if (FAILED(hr))
{
wprintf(L"Failed to Get the Requested Range, HR: 0x%08x\n", hr);
uiaRange = NULL;
}
// We can validly get a NULL range, if the selection is Empty, or nothing is Visible
// And even if we have an error, we should wrap the NULL so we have a Range object
// If the Range gets created it takes ownership of the IUIAutomationTextRange object
retVal = new Range(uiaRange);
if (retVal == NULL)
{
wprintf(L"Failed to create internal Range object\n");
if (uiaRange != NULL)
{
uiaRange->Release();
}
}
return retVal;
}
HRESULT GetTextPattern()
{
HRESULT hr = _element->GetCurrentPatternAs(UIA_TextPattern2Id, IID_PPV_ARGS(&_textPattern));
if (FAILED(hr))
{
wprintf(L"Failed to Get Text Pattern, HR: 0x%08x\n", hr);
}
else if (_textPattern == NULL)
{
wprintf(L"Element does not actually support Text Pattern 2\n");
hr = E_FAIL;
}
return hr;
}
IUIAutomationTextPattern2 *_textPattern;
IUIAutomationElement *_element;
};
// Will search an element itself and all its children and descendants for the first element that supports Text Pattern 2
HRESULT FindTextPatternElement(_In_ IUIAutomationElement *element, _Outptr_result_maybenull_ IUIAutomationElement **textElement)
{
HRESULT hr = S_OK;
// Create a condition that will be true for anything that supports Text Pattern 2
IUIAutomationCondition* textPatternCondition;
VARIANT trueVar;
trueVar.vt = VT_BOOL;
trueVar.boolVal = VARIANT_TRUE;
hr = _automation->CreatePropertyCondition(UIA_IsTextPattern2AvailablePropertyId, trueVar, &textPatternCondition);
if (FAILED(hr))
{
wprintf(L"Failed to CreatePropertyCondition, HR: 0x%08x\n", hr);
}
else
{
// Actually do the search
hr = element->FindFirst(TreeScope_Subtree, textPatternCondition, textElement);
if (FAILED(hr))
{
wprintf(L"FindFirst failed, HR: 0x%08x\n", hr);
}
else if (*textElement == NULL)
{
wprintf(L"No element supporting TextPattern2 found.\n");
hr = E_FAIL;
}
textPatternCondition->Release();
}
return hr;
}
void Usage()
{
wprintf(L"Usage:\n\n");
wprintf(L"UiaDocumentClient [hwnd]\n\n");
wprintf(L"Explore the text pattern for the specific [hwnd] (in hex)\n");
wprintf(L"If no [hwnd] is supplied, it will get the element under the mouse pointer.\n\n");
}
int _cdecl wmain(_In_ int argc, _In_reads_(argc) WCHAR* argv[])
{
UNREFERENCED_PARAMETER(argv);
// We only take 0 or 1 argument, since the program name is the first argument this
// means any arg count more than 2 is wrong.
if (argc > 2)
{
Usage();
}
else
{
// Initialize COM before using UI Automation
HRESULT hr = CoInitialize(NULL);
if (FAILED(hr))
{
wprintf(L"CoInitialize failed, HR:0x%08x\n", hr);
}
else
{
hr = CoCreateInstance(__uuidof(CUIAutomation8), NULL,
CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&_automation));
if (FAILED(hr))
{
wprintf(L"Failed to create a CUIAutomation8, HR: 0x%08x\n", hr);
}
else
{
IUIAutomationElement *element = NULL;
if (argc == 1)
{
wprintf( L"Getting element at cursor in 3 seconds...\n" );
Sleep(3000);
POINT pt;
GetCursorPos(&pt);
hr = _automation->ElementFromPoint(pt, &element);
if (FAILED(hr))
{
wprintf( L"Failed to ElementFromPoint, HR: 0x%08x\n\n", hr );
}
}
else if (argc == 2)
{
long hwndAsLong;
int ret = swscanf_s(argv[1], L"%x", &hwndAsLong);
if (ret != 1)
{
wprintf( L"The hwnd parameter needs to be a number in hex.\n" );
Usage();
hr = E_INVALIDARG;
}
else
{
HWND hwnd = static_cast<HWND>(LongToHandle(hwndAsLong));
if (!IsWindow(hwnd))
{
wprintf( L"The hwnd specifier 0x%08x is not a valid HWND.\n", hwndAsLong );
hr = E_INVALIDARG;
}
else
{
hr = _automation->ElementFromHandle(hwnd, &element);
if (FAILED(hr))
{
wprintf( L"Failed to ElementFromHandle, HR: 0x%08x\n\n", hr );
}
}
}
}
if (SUCCEEDED(hr) && element != NULL)
{
IUIAutomationElement *textElement = NULL;
hr = FindTextPatternElement(element, &textElement);
if (SUCCEEDED(hr) && textElement != NULL)
{
// The TextPatternExplorer takes ownership of the textElement, so no need to release it here
TextPatternExplorer explorer(textElement);
explorer.Run();
}
element->Release();
}
_automation->Release();
}
CoUninitialize();
}
}
return 0;
}