diff --git a/docs/source/libraries/desktop_windows/index.rst b/docs/source/libraries/desktop_windows/index.rst
deleted file mode 100644
index 21641dffec..0000000000
--- a/docs/source/libraries/desktop_windows/index.rst
+++ /dev/null
@@ -1,22 +0,0 @@
-###############
-Desktop.Windows
-###############
-
-Automate Windows desktop applications.
-
-.. toctree::
- :maxdepth: 1
- :hidden:
-
- python
-
-Keywords
-========
-
-🔗 Direct link to `keyword documentation <../../libdoc/RPA_Desktop_Windows.html>`_.
-
--------
-
-.. raw:: html
-
-
diff --git a/docs/source/libraries/desktop_windows/python.rst b/docs/source/libraries/desktop_windows/python.rst
deleted file mode 100644
index 3c7aa423f5..0000000000
--- a/docs/source/libraries/desktop_windows/python.rst
+++ /dev/null
@@ -1,12 +0,0 @@
-##########
-Python API
-##########
-
-********
-Windows
-********
-
-.. autoclass:: RPA.Desktop.Windows.Windows
- :members:
- :inherited-members:
- :undoc-members:
diff --git a/docs/source/releasenotes.rst b/docs/source/releasenotes.rst
index 65d74ff5ab..41b5be0507 100644
--- a/docs/source/releasenotes.rst
+++ b/docs/source/releasenotes.rst
@@ -12,17 +12,32 @@ Latest versions
`Upcoming release `_
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+- **Breaking:** Remove the long-deprecated ``RPA.Desktop.Windows`` library (pywinauto-based).
+ It has emitted a deprecation warning pointing to ``RPA.Windows`` since being deprecated in
+ favor of it, and is no longer maintained. Use ``RPA.Windows`` for all Windows UI automation
+ going forward (fixes :issue:`1322`).
- **Security:** ``RPA.Archive``: Fix a Zip Slip path traversal vulnerability (CWE-22) in
``Extract Archive`` — archive members with path traversal sequences (e.g.
``../../evil.py``) could previously be extracted outside the requested destination
directory. Extraction now validates that every member resolves within the destination
before extracting, raising ``ValueError`` otherwise (fixes :issue:`1339`, :issue:`1340`).
-
+- ``rpaframework-core``: Fix the Windows locator parser silently mis-tokenizing a strategy
+ when the locator value contained a stray ``locator=`` prefix or an unmatched quote
+ character (e.g. produced a bogus ``locator='executable`` strategy instead of recognizing
+ ``executable:``), with a clearer warning surfaced when this happens (fixes :issue:`1323`).
+- ``RPA.Desktop``: ``Highlight Elements`` now returns the list of matched element regions
+ instead of ``None``, exposing coordinates that were already being computed internally
+ (fixes :issue:`1324`).
+- ``rpaframework-core``: Match ``executable:`` locators case-insensitively. Windows file
+ names are case-insensitive, but the comparison was not, so ``executable:notepad.exe``
+ could not find a process that Windows lists as ``Notepad.exe`` — as it does on Windows 11.
+ ``handle:`` matching is numeric and is unchanged.
- **Security:** Bump ``soupsieve`` ≥2.8.4 (HIGH — memory exhaustion via large
comma-separated selector lists, CVE-2026-49476) in the root and ``rpaframework``
lock files.
-- ``rpaframework`` **32.0.2**
+- ``rpaframework-core`` **13.0.2**
+- ``rpaframework`` **33.0.0**
`Released `_
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
diff --git a/examples/README.md b/examples/README.md
index 047503a080..2b821ccb38 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -3,13 +3,3 @@
## manipulating-pdf
Python syntax example of working with PDFs.
-
-## windows-basics
-
-Python and RFW syntax example of Windows basic operations with Calculator and Notepad.
-
-## windows-uidemo-application
-
-Python syntax example of UIPath's UIDemo.exe
-
-The environment variable `UIDEMO_EXE` needs to be set to the absolute path of the file.
diff --git a/examples/windows-basics/mytextfile.txt b/examples/windows-basics/mytextfile.txt
deleted file mode 100644
index a2c76e4c17..0000000000
--- a/examples/windows-basics/mytextfile.txt
+++ /dev/null
@@ -1 +0,0 @@
-Story of the Windows RPA
\ No newline at end of file
diff --git a/examples/windows-basics/task.py b/examples/windows-basics/task.py
deleted file mode 100644
index 552e61dce5..0000000000
--- a/examples/windows-basics/task.py
+++ /dev/null
@@ -1,93 +0,0 @@
-""" Windows Calculator robot. """
-import logging
-from pathlib import Path
-import sys
-from time import sleep
-
-from RPA.Desktop.Windows import Windows
-
-library = None
-stdout = logging.StreamHandler(sys.stdout)
-
-logging.basicConfig(
- level=logging.DEBUG,
- format="[{%(filename)s:%(lineno)d} %(levelname)s - %(message)s",
- handlers=[stdout],
-)
-
-LOGGER = logging.getLogger(__name__)
-
-
-def result_should_be(expected):
- element = library.get_element("CalculatorResults")
- value = int(element["rich_text"].replace("Display is ", ""))
- LOGGER.info("Got %s value, expected value %s" % (value, expected))
- assert value == expected
-
-
-def using_mouse():
- library.mouse_click("One")
- library.mouse_click("Plus")
- library.mouse_click("Five")
- library.mouse_click("Equals")
- result_should_be(6)
-
-
-def using_keys():
- library.send_keys("320{+}480{=}")
- result_should_be(800)
-
-
-def open_navigation(navigation_item):
- library.wait_for_element("Open Navigation")
- library.mouse_click("Open Navigation")
- library.refresh_window()
- library.mouse_click(navigation_item)
- library.refresh_window()
-
-
-def open_calculator():
- library.open_executable("calc.exe", "Calculator")
- open_navigation("Standard Calculator")
- controls, elements = library.get_window_elements()
- LOGGER.info("Printing elements")
- for elem in elements:
- LOGGER.info(elem)
- LOGGER.info("Printing controls")
- for ctrl in controls:
- LOGGER.info(ctrl)
- using_mouse()
- library.mouse_click("Clear")
- using_keys()
- open_navigation("Date Calculation Calculator")
- sleep(1)
-
-
-def minimize_maximize(windowtitle):
- library.minimize_dialog()
- sleep(1)
- library.restore_dialog()
- sleep(1)
- library.minimize_dialog()
- sleep(1)
- library.restore_dialog(windowtitle)
-
-
-def open_text_file():
- filepath = Path(__file__).parent / "mytextfile.txt"
- library.open_file(filepath.absolute(), "Notepad", wildcard=True)
- element = library.get_element("class:Edit")
- assert element["legacy"]["Value"] == "Story of the Windows RPA"
-
-
-if __name__ == "__main__":
- library = Windows()
- try:
- open_text_file()
- open_calculator()
- minimize_maximize("Calculator")
- open_navigation("Standard Calculator")
- library.mouse_click("Clear")
- sleep(3)
- finally:
- library.close_all_applications()
diff --git a/examples/windows-basics/task.robot b/examples/windows-basics/task.robot
deleted file mode 100644
index ecfd8b6838..0000000000
--- a/examples/windows-basics/task.robot
+++ /dev/null
@@ -1,28 +0,0 @@
-*** Settings ***
-Library RPA.Desktop.Windows
-Task Teardown Close All Applications
-
-*** Keywords ***
-Connect to Calculator
- [Arguments] ${handle}
- Connect By Handle ${${handle}} existing_app=True
- Send Keys %2
- Sleep 2s
- Send Keys %1
- Sleep 2s
-
-*** Tasks ***
-Use calculator
- Open Executable calc.exe Calculator
- ${controls} ${elements}= Get Window Elements
- FOR ${elem} IN @{elements}
- Run Keyword If "${elem}[control_type]" == "Window" and "${elem}[name]" == "Calculator" and "${elem}[class_name]" == "ApplicationFrameWindow"
- ... Connect To Calculator ${elem}[handle]
- END
-
-Use Notepad
- ${app}= Open From Search notepad.exe Notepad wildcard=True
- Log Many ${app}
- Open Dialog Notepad wildcard=True existing_app=True
- ${process}= Process ID Exists 25032
- Run Keyword If ${process} Log Process exists
diff --git a/examples/windows-uidemo-application/task.py b/examples/windows-uidemo-application/task.py
deleted file mode 100644
index 26b257ff5e..0000000000
--- a/examples/windows-uidemo-application/task.py
+++ /dev/null
@@ -1,108 +0,0 @@
-""" An example robot. """
-import logging
-import os
-from pathlib import Path
-import sys
-from time import sleep
-
-from RPA.Desktop.Windows import Windows
-from variables import CURRENT_DATE, CURRENT_TIME, CASH_IN, ON_US, NOT_US
-
-
-application_path = Path(os.getenv("UIDEMO_EXE"))
-if not os.path.exists(application_path):
- raise ValueError("Set path to UIDemo.exe with environment variable UIDEMO_EXE")
-
-library = None
-stdout = logging.StreamHandler(sys.stdout)
-
-logging.basicConfig(
- level=logging.DEBUG,
- format="[{%(filename)s:%(lineno)d} %(levelname)s - %(message)s",
- handlers=[stdout],
-)
-
-LOGGER = logging.getLogger(__name__)
-
-
-def set_slider_value(locator, slidervalue):
- element, _ = library.find_element(locator)
- if element and len(element) == 1:
- target_element = element[0]
- else:
- raise ValueError("Did not find unique element")
- left, top, right, bottom = library._get_element_coordinates(target_element)
- width = right - left
- middle = top + int((bottom - top) / 2)
- point = left + int(width * slidervalue)
- library.mouse_click_coords(point, middle)
-
-
-def do_the_application_login():
- library.open_executable(r"%s" % application_path, "UiDemo")
- library.type_into("user", "admin")
- library.type_into("pass", "password")
- library.mouse_click("name:'Log In' and type:Button")
- library.open_dialog("UIDemo")
-
-
-def click_transactions():
- library.mouse_click("name:'Split Deposit' and type:RadioButton")
- library.mouse_click("name:Withdrawal and type:RadioButton")
- library.mouse_click("name:Deposit and type:RadioButton")
-
-
-def click_configurations():
- library.mouse_click("name:'Use Cash Count' and type:CheckBox", focus="topleft")
- library.mouse_click("name:'Use Both' and type:RadioButton", focus="topleft")
- library.mouse_click("name:'Use Amount' and type:RadioButton", focus="topleft")
- library.mouse_click("name:'Use Piece Count' and type:RadioButton", focus="topleft")
- library.mouse_click(
- "name:'Reverse Denomination' and type:CheckBox", focus="topleft"
- )
- library.mouse_click("name:'Eliminate $2' and type:CheckBox", focus="topleft")
-
-
-def click_settings():
- library.mouse_click("name:GraphLabel and type:CheckBox")
- library.mouse_click("name:TrainingTip and type:CheckBox")
- library.mouse_click("name:EnableAdditional and type:CheckBox")
- library.mouse_click("name:ChangeTItle and type:CheckBox")
- library.mouse_click("name:ShowExcluding and type:CheckBox")
-
-
-def type_cash(cash_in, on_us_check, not_on_us_check):
- library.type_into(CASH_IN, cash_in)
- library.type_into(ON_US, on_us_check)
- library.type_into(NOT_US, not_on_us_check)
-
-
-def main():
- do_the_application_login()
- currentdate = library.get_text(CURRENT_DATE)
- currenttime = library.get_text(CURRENT_TIME)
- LOGGER.info(f"CURRENT DATE: {currentdate['children_texts']}")
- LOGGER.info(f"CURRENT TIME: {currenttime['children_texts']}")
-
- click_transactions()
- click_configurations()
- type_cash(500, 300, 100)
- click_settings()
-
- set_slider_value("id:uiScaleSlider and type:Slider", 0.2)
- sleep(5)
- winlist = library.get_window_list()
- for w in winlist:
- LOGGER.info(w)
- library.refresh_window()
- set_slider_value("id:uiScaleSlider and type:Slider", 0.5)
- sleep(5)
- LOGGER.info("Done.")
-
-
-if __name__ == "__main__":
- library = Windows()
- try:
- main()
- finally:
- library.close_all_applications()
diff --git a/examples/windows-uidemo-application/variables.py b/examples/windows-uidemo-application/variables.py
deleted file mode 100644
index 8bae40666c..0000000000
--- a/examples/windows-uidemo-application/variables.py
+++ /dev/null
@@ -1,6 +0,0 @@
-CURRENT_DATE = "id:DateLabel"
-CURRENT_TIME = "id:TimeLabel"
-
-CASH_IN = "id:cashintb"
-ON_US = "id:onustb"
-NOT_US = "id:notonustb"
\ No newline at end of file
diff --git a/invocations/config.py b/invocations/config.py
index 2fcc80548e..b13178a76b 100644
--- a/invocations/config.py
+++ b/invocations/config.py
@@ -407,6 +407,7 @@ def install_local(ctx, package, extra=None, all_extras=False):
"""
backup_dependency_files(ctx)
valid_packages = get_package_paths()
+ pkg_root = get_current_package_root(ctx)
if not package:
package = valid_packages.keys()
opt_dependencies = []
@@ -417,11 +418,15 @@ def install_local(ctx, package, extra=None, all_extras=False):
pkg_name = "main"
else:
pkg_name = pkg
- pkg_root = get_current_package_root(ctx)
dependency_path = PACKAGES_ROOT / pkg_name
+ if dependency_path.resolve() == pkg_root.resolve():
+ continue
opt_dependencies.append(str(relative_path(pkg_root, dependency_path)))
- add_arg = " ".join(opt_dependencies)
- shell.uv(ctx, f"add --editable {add_arg}")
+ if not opt_dependencies:
+ print("No external local packages to install.")
+ return
+ add_arg = " ".join(opt_dependencies)
+ shell.uv(ctx, f"add --editable {add_arg}", in_stream=False)
def relative_path(first_path: Path, second_path: Path) -> Path:
diff --git a/invocations/util.py b/invocations/util.py
index 0bdf992017..4ed8c71b28 100644
--- a/invocations/util.py
+++ b/invocations/util.py
@@ -47,8 +47,12 @@ def get_package_paths():
for project_toml in project_tomls:
toml_path = Path(project_toml)
project_config = toml.load(toml_path)
+ if "tool" in project_config and "poetry" in project_config.get("tool", {}):
+ name = str(project_config["tool"]["poetry"]["name"])
+ else:
+ name = str(project_config["project"]["name"])
package_paths[toml_path.parent.name] = {
- "name": str(project_config["tool"]["poetry"]["name"]),
+ "name": name,
"path": toml_path.parent.resolve(),
}
return package_paths
diff --git a/packages/core/pyproject.toml b/packages/core/pyproject.toml
index 32127f24a5..5cf9dd17ea 100644
--- a/packages/core/pyproject.toml
+++ b/packages/core/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "rpaframework-core"
-version = "13.0.1"
+version = "13.0.2"
description = "Core utilities used by RPA Framework"
authors = [{name = "RPA Framework", email = "rpafw@robocorp.com"}]
license = {text = "Apache-2.0"}
diff --git a/packages/core/src/RPA/core/windows/locators.py b/packages/core/src/RPA/core/windows/locators.py
index db55db24c2..06df8c2379 100644
--- a/packages/core/src/RPA/core/windows/locators.py
+++ b/packages/core/src/RPA/core/windows/locators.py
@@ -188,6 +188,11 @@ class MatchObject:
PATH_SEP = "|" # path locator index separator
QUOTE = '"' # enclosing quote character (double-quote; single-quote also accepted)
_LOCATOR_REGEX = re.compile(r"""\S*"[^"]+"|\S*'[^']+'|\S+""", re.IGNORECASE)
+ # Matches a stray `locator=` keyword-argument prefix (optionally followed by an
+ # unmatched quote character) that can end up glued onto the strategy token, e.g.
+ # when a value like `locator='executable:...` gets swallowed whole by
+ # `_LOCATOR_REGEX` due to its unclosed quote.
+ _STRAY_LOCATOR_PREFIX_REGEX = re.compile(r"^locator=['\"]?", re.IGNORECASE)
_LOGGER = logging.getLogger(__name__)
locators: List[Tuple] = field(default_factory=list)
@@ -232,8 +237,19 @@ def handle_locator_part(
default_values.append(part_text)
return
- control_strategy = self._WINDOWS_LOCATOR_STRATEGIES.get(strategy)
+ stray_match = self._STRAY_LOCATOR_PREFIX_REGEX.match(strategy)
+ clean_strategy = strategy[stray_match.end() :] if stray_match else strategy
+
+ control_strategy = self._WINDOWS_LOCATOR_STRATEGIES.get(clean_strategy)
if control_strategy:
+ if stray_match:
+ self._LOGGER.warning(
+ "Locator part %r looked malformed (stray %r prefix); using"
+ " strategy %r instead.",
+ part_text,
+ stray_match.group(),
+ clean_strategy,
+ )
if default_values:
add_locator("Name", " ".join(default_values))
default_values.clear()
@@ -386,7 +402,18 @@ def _get_control_from_listed_windows(
search_params = search_params.copy() # to keep idempotent behaviour
win_value = search_params.pop(param_type)
window_list = self.ctx.list_windows()
- matches = [win for win in window_list if win[win_type] == win_value]
+ if param_type == "executable":
+ # Windows file names are case-insensitive, so `executable:notepad.exe`
+ # has to match a process listed as `Notepad.exe`. Only the executable
+ # is compared this way; `handle` is numeric and matched as-is.
+ win_value_folded = str(win_value).casefold()
+ matches = [
+ win
+ for win in window_list
+ if str(win[win_type]).casefold() == win_value_folded
+ ]
+ else:
+ matches = [win for win in window_list if win[win_type] == win_value]
if not matches:
raise WindowControlError(
f"Could not locate window with {param_type} {win_value!r}"
diff --git a/packages/core/tests/python/test_windows.py b/packages/core/tests/python/test_windows.py
index 7d0120432b..e1d9b5d17e 100644
--- a/packages/core/tests/python/test_windows.py
+++ b/packages/core/tests/python/test_windows.py
@@ -3,7 +3,7 @@
import pytest
-from RPA.core.windows.context import ElementNotFound
+from RPA.core.windows.context import ElementNotFound, WindowControlError
from RPA.core.windows.locators import LocatorMethods, MatchObject
@@ -96,6 +96,14 @@ class TestMatchObject:
"Calculator > path:2|3|2|8|2",
[("Name", "Calculator", 0), ("path", [2, 3, 2, 8, 2], 1)],
),
+ (
+ "locator='executable:AsdfConfigurator.exe",
+ [("executable", "AsdfConfigurator.exe", 0)],
+ ), # stray `locator=` keyword-arg prefix + unmatched quote (issue #1323)
+ (
+ "LOCATOR=\"executable:AsdfConfigurator.exe",
+ [("executable", "AsdfConfigurator.exe", 0)],
+ ), # case-insensitive, double-quote variant
],
)
def test_match_object(self, locator, locators):
@@ -126,3 +134,54 @@ def test_get_control_from_path(self, library, search_params, should_raise):
with should_raise:
leaf = library._get_control_from_path(search_params, root_control)
assert leaf == child22
+
+ @pytest.mark.parametrize(
+ "locator_value, listed_name, should_match",
+ [
+ # Windows file names are case-insensitive, so the case a user writes
+ # must not decide whether the window is found. Windows 11 lists
+ # Notepad as "Notepad.exe" while everyone writes "notepad.exe".
+ ("notepad.exe", "Notepad.exe", True),
+ ("Notepad.exe", "notepad.exe", True),
+ ("NOTEPAD.EXE", "Notepad.exe", True),
+ ("notepad.exe", "notepad.exe", True),
+ # Different executables must still not match each other.
+ ("notepad.exe", "wordpad.exe", False),
+ ],
+ )
+ def test_executable_match_is_case_insensitive(
+ self, library, locator_value, listed_name, should_match
+ ):
+ library.ctx.list_windows.return_value = [
+ {"name": listed_name, "title": "Untitled - Notepad", "handle": 1}
+ ]
+
+ should_raise = (
+ nullcontext() if should_match else pytest.raises(WindowControlError)
+ )
+ with should_raise, mock.patch.object(
+ library, "_get_control_from_params"
+ ) as get_control:
+ library._get_control_from_listed_windows(
+ {"executable": locator_value}, param_type="executable", win_type="name"
+ )
+ # The window title of the matched process is what gets searched for.
+ assert get_control.call_args[0][0]["Name"] == "Untitled - Notepad"
+
+ def test_handle_match_stays_exact(self, library):
+ """Only `executable` is folded - `handle` is numeric and must not be."""
+ library.ctx.list_windows.return_value = [
+ {"name": "notepad.exe", "title": "Untitled - Notepad", "handle": 12345}
+ ]
+
+ with mock.patch.object(library, "_get_control_from_params") as get_control:
+ library._get_control_from_listed_windows(
+ {"handle": 12345}, param_type="handle", win_type="handle"
+ )
+ assert get_control.call_args[0][0]["Name"] == "Untitled - Notepad"
+
+ with pytest.raises(WindowControlError):
+ library._get_control_from_listed_windows(
+ {"handle": 99999}, param_type="handle", win_type="handle"
+ )
+
diff --git a/packages/core/uv.lock b/packages/core/uv.lock
index 480fb1fdff..890f46285a 100644
--- a/packages/core/uv.lock
+++ b/packages/core/uv.lock
@@ -675,7 +675,7 @@ wheels = [
[[package]]
name = "rpaframework-core"
-version = "13.0.1"
+version = "13.0.2"
source = { editable = "." }
dependencies = [
{ name = "httpx" },
diff --git a/packages/main/pyproject.toml b/packages/main/pyproject.toml
index e2e97feafa..1640605066 100644
--- a/packages/main/pyproject.toml
+++ b/packages/main/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "rpaframework"
-version = "32.0.2"
+version = "33.0.0"
description = "A collection of tools and libraries for RPA"
authors = [{name = "RPA Framework", email = "rpafw@robocorp.com"}]
license = {text = "Apache-2.0"}
diff --git a/packages/main/src/RPA/Desktop/Windows.py b/packages/main/src/RPA/Desktop/Windows.py
deleted file mode 100644
index 4641bdf375..0000000000
--- a/packages/main/src/RPA/Desktop/Windows.py
+++ /dev/null
@@ -1,2071 +0,0 @@
-# pylint: disable=c-extension-no-member
-# pylint: disable=too-many-lines
-# pylint: disable=possibly-used-before-assignment
-# pylint: disable=consider-using-dict-items
-# pylint: disable=used-before-assignment
-# pylint: disable=consider-using-min-builtin
-# pylint: disable=consider-using-max-builtin
-from enum import Enum
-import json
-import logging
-import os
-import platform
-import re
-import subprocess
-import time
-from pathlib import Path
-from typing import Any, Optional
-
-from RPA.Desktop import Desktop
-from RPA.Desktop.Clipboard import Clipboard
-from RPA.Desktop.OperatingSystem import OperatingSystem
-from RPA.core.geometry import Region
-from RPA.core.helpers import delay, clean_filename
-from RPA.core.locators import ImageLocator
-from RPA.core.logger import deprecation
-
-
-if platform.system() == "Windows":
- import ctypes
- import win32api
- import win32com.client
- import win32con
- import win32security
- import pywinauto
- import win32gui
- from comtypes import COMError
-else:
- logging.getLogger(__name__).warning(
- "RPA.Desktop.Windows library works only on Windows platform"
- )
-
-
-def write_element_info_as_json(
- elements: Any, filename: str, path: str = "output/json"
-) -> None:
- """Write list of elements into json file
-
- :param elements: list of elements to write
- :param filename: output file name
- :param path: output directory, defaults to "output/json"
- """
- elements = elements if isinstance(elements, list) else [elements]
- filename = Path(f"{path}/{filename}.json")
- os.makedirs(filename.parent, exist_ok=True)
- with open(filename, "w", encoding="utf-8") as outfile:
- json.dump(elements, outfile, indent=4, sort_keys=True)
-
-
-class ElementNotFoundError(Exception):
- """Raised when expected element is not found"""
-
-
-class MenuItemNotFoundError(Exception):
- """Raised when expected menu item is not found"""
-
-
-class UnknownWindowsBackendError(Exception):
- """Raised when unknown Windows backend is set"""
-
-
-class ApplicationNotStarted(Exception):
- """Raised when application fails to start"""
-
-
-SUPPORTED_BACKENDS = ["uia", "win32"]
-WINDOWS_LOCATOR_STRATEGIES = {
- "name": "name",
- "class_name": "class_name",
- "class": "class_name",
- "control_type": "control_type",
- "type": "control_type",
- "automation_id": "automation_id",
- "id": "automation_id",
- "partial name": "partial name",
- "regexp": "regexp",
- "parent": "parent",
-}
-
-
-class Speed(Enum):
- """Values for pywinauto Timings"""
-
- DEFAULT = 1
- SLOW = 2
- FAST = 3
-
-
-def to_speed(value):
- """Convert value to Speed enum."""
- if isinstance(value, Speed):
- return value
-
- sanitized = str(value).lower().strip().replace(" ", "_")
- try:
- return Speed[sanitized]
- except KeyError as err:
- raise ValueError(f"Unknown speed: {value}") from err
-
-
-class Windows(OperatingSystem):
- """`Windows` is a library for managing the Windows operating system.
-
- **DEPRECATION WARNING! USE RPA.Windows library instead.**
-
- For Windows desktop automation Robocorp recommends the ``RPA.Windows`` library.
-
- No further updates will be released for this library and new functionality will continue
- to be developed in ``RPA.Windows`` library.
-
- **Running Windows applications**
-
- Windows applications can be started in several ways. The library supports
- the following keywords:
-
- - Open Application (dispatch Office applications)
- - Open File (open the file as process which opens the associated application)
- - Open Executable (uses pywinauto start)
- - Open Using Run Dialog (uses Windows run dialog)
- - Open From Search (uses Windows search dialog)
-
- **Locators**
-
- `Locator` is used to identify the element for interaction - usually for a mouse click.
-
- Locators can investigated for application once it has been opened by calling
- the keyword `get_windows_elements` which can store locator information into JSON file
- and `screenshot` of the element into an image file.
-
- **Identifying locator**
-
- The element needs to be identified by a unique method, for example, "Three" for button 3
- in the Calculator application. It can be given either as `Three` or `name:Three`.
-
- Possible search criterias:
-
- - name
- - class (class_name)
- - type (control_type)
- - id (automation_id)
- - any if none was defined
-
- The current method of inspecting elements on Windows is `inspect.exe` which is part
- of `Windows SDK `_.
-
- **Keyboard**
-
- The keyword `send_keys` can be used to send keys to the active window. The keyword
- `type_keys` sends keys to the active window element.
-
- Special key codes are documented on `pywinauto `_
- documentation page.
-
- **FAQ**
-
- Q. I see error message `AttributeError: module 'win32com.gen_py.00020813-0000-0000-C000-000000000046x0x1x9' has no attribute 'CLSIDToClassMap'`
-
- A. From PowerShell run this command: `Remove-Item -path $env:LOCALAPPDATA\\Temp\\gen_py -recurse`
-
- **Examples**
-
- **Robot Framework**
-
- .. code-block:: robotframework
-
- *** Settings ***
- Library RPA.Desktop.Windows
- Suite Teardown Close all applications
-
- *** Tasks ***
- Open Calculator using run dialog
- ${result}= Open using run dialog calc.exe Calculator
- ${result}= Get Window Elements
- Send Keys 5*2=
- ${result}= Get element partial name:Display is
- Log Many ${result}
- ${result}= Get element rich text id:CalculatorResults
- Should Be Equal As Strings ${result} Display is 10
- ${result}= Get element rectangle partial name:Display is
- ${result}= Is Element Visible CalculatorResults
- ${result}= Is Element Enabled partial name:Display is
-
- **Python**
-
- .. code-block:: python
-
- from RPA.Desktop.Windows import Windows
-
- win = Windows()
-
- def open_calculator():
- win.open_from_search("calc.exe", "Calculator")
- elements = win.get_window_elements()
-
- def make_calculations(expression):
- win.send_keys(expression)
- result = win.get_element_rich_text('id:CalculatorResults')
- return int(result.strip('Display is '))
-
- if __name__ == "__main__":
- open_calculator()
- exp = '5*2='
- result = make_calculations(exp)
- print(f"Calculation result of '{exp}' is '{result}'")
- win.close_all_applications()
- """ # noqa: E501
-
- ROBOT_LIBRARY_SCOPE = "GLOBAL"
- ROBOT_LIBRARY_DOC_FORMAT = "REST"
-
- def __init__(self, backend: str = "uia") -> None:
- deprecation(
- "`RPA.Desktop.Windows` got deprecated and will be no longer maintained, "
- "please use `RPA.Windows` instead "
- "(https://robocorp.com/docs-robot-framework/libraries/rpa-framework/rpa-windows)"
- )
-
- OperatingSystem.__init__(self)
- self._apps = {}
- self._app_instance_id = 0
- self._active_app_instance = -1
- self.set_windows_backend(backend)
- self.app = None
- self.dlg = None
- self.windowtitle = None
- self.logger = logging.getLogger(__name__)
- self.clipboard = Clipboard()
- self.elements = None
- self.controls = None
-
- def set_windows_backend(self, backend: str) -> None:
- """Set Windows backend which is used to interact with Windows
- applications
-
- Allowed values defined by `SUPPORTED_BACKENDS`
-
- :param backend: name of the backend to use
-
- Example:
-
- .. code-block:: robotframework
-
- Set Windows Backend uia
- Open Executable calc.exe Calculator
- Set Windows Backend win32
- Open Executable calc.exe Calculator
-
- """
- if backend and backend.lower() in SUPPORTED_BACKENDS:
- self._backend = backend.lower()
- else:
- raise UnknownWindowsBackendError(
- "Unsupported Windows backend: %s" % backend
- )
-
- def _add_app_instance(
- self,
- app: Any = None,
- dialog: bool = True,
- params: dict = None,
- ) -> int:
- params = params or {}
- self._app_instance_id += 1
- process_id = None
- handle = None
- if app:
- self.app = app
- if hasattr(app, "process"):
- process_id = app.process
- handle = win32gui.GetForegroundWindow()
-
- default_params = {
- "app": app,
- "id": self._app_instance_id,
- "dialog": dialog,
- "process_id": process_id,
- "handle": handle,
- "dispatched": False,
- }
-
- self._apps[self._app_instance_id] = {**default_params, **params}
-
- self.logger.debug(
- "Added app instance %s: %s",
- self._app_instance_id,
- self._apps[self._app_instance_id],
- )
- self._active_app_instance = self._app_instance_id
- return self._active_app_instance
-
- def switch_to_application(self, app_id: int) -> None:
- """Switch to application by id.
-
- :param app_id: application's id
- :raises ValueError: if application is not found by given id
-
- Example:
-
- .. code-block:: robotframework
-
- ${app1} Open Application Excel
- ${app2} Open Application Word
- Switch To Application ${app1}
-
- """
- if app_id and app_id in self._apps.keys():
- app = self.get_app(app_id)
- self._active_app_instance = app_id
- self.app = app["app"]
- if "windowtitle" in app:
- self.open_dialog(app["windowtitle"], existing_app=True)
- delay(0.5)
- self.restore_dialog(app["windowtitle"])
- else:
- raise ValueError(f"No open application with id '{app_id}'")
-
- def get_open_applications(self):
- """Get list of all open applications
-
- Returns a dictionary
-
- Example:
-
- .. code-block:: robotframework
-
- ${app1} Open Application Excel
- ${app2} Open Executable calc.exe Calculator
- ${app3} Open File /path/to/myfile.txt
- &{apps} Get Open Applications
-
- """
- return self._apps
-
- def get_app(self, app_id: int = None) -> Any:
- """Get application object by id
-
- By default returns active_application application object.
-
- :param app_id: id of the application to get, defaults to None
- :return: application object
-
- Example:
-
- .. code-block:: robotframework
-
- ${app1} Open Application Excel
- &{appdetails} Get App ${app1}
-
- """
- if app_id is None and self._active_app_instance != -1:
- return self._apps[self._active_app_instance]
- else:
- return self._apps[app_id]
-
- def open_application(self, application: str) -> int:
- """Open application by dispatch method
-
- This keyword is used to launch Microsoft applications like
- Excel, Word, Outlook and Powerpoint.
-
- :param application: name of the application as `str`
- :return: application instance id
-
- Example:
-
- .. code-block:: robotframework
-
- ${app1} Open Application Excel
- ${app2} Open Application Word
-
- """
- self.logger.info("Open application: %s", application)
- app = win32com.client.gencache.EnsureDispatch(f"{application}.Application")
- app.Visible = True
- # show eg. file overwrite warning or not
- if hasattr(self.app, "DisplayAlerts"):
- app.DisplayAlerts = False
- params = {
- "dispatched": True,
- "startkeyword": "Open Application",
- }
- return self._add_app_instance(app, dialog=False, params=params)
-
- def open_file(
- self,
- filename: str,
- windowtitle: str = None,
- wildcard: bool = False,
- timeout: int = 10,
- ) -> Optional[int]:
- """Open associated application when opening file
-
- Keyword `Open Dialog` is used if `windowtitle` is given.
-
- :param filename: path to file
- :param windowtitle: name of the window
- :param wildcard: set True for inclusive window title search, default False
- :param timeout: time to wait for dialog to appear
- :return: application id or None
-
- Example:
-
- .. code-block:: robotframework
-
- ${app1} Open File /path/to/myfile.txt
-
- """
- self.logger.info("Open file: %s", filename)
- if platform.system() == "Windows":
- # pylint: disable=no-member
- os.startfile(filename)
- elif platform.system() == "Darwin":
- subprocess.call(["open", filename])
- else:
- subprocess.call(["xdg-open", filename])
-
- app_instance = None
- if windowtitle:
- app_instance = self.open_dialog(
- windowtitle, wildcard=wildcard, timeout=timeout
- )
- if app_instance > 0:
- self._apps[app_instance]["executable"] = filename
- self._apps[app_instance]["startkeyword"] = "Open File"
-
- return app_instance
-
- def open_executable(
- self,
- executable: str,
- windowtitle: str,
- backend: str = None,
- work_dir: str = None,
- wildcard: bool = False,
- ) -> int:
- """Open Windows executable. Window title name is required
- to get handle on the application.
-
- :param executable: name of the executable
- :param windowtitle: name of the window
- :param backend: set Windows backend, default None means using
- library default value
- :param work_dir: path to working directory, default None
- :param wildcard: set True for inclusive window title search, default False
- :return: application instance id
-
- Example:
-
- .. code-block:: robotframework
-
- ${app1} Open Executable calc.exe Calculator
- ${app2} Open Executable notepad.exe Notepad wildcard=True
-
- """
- self.logger.info("Opening executable: %s - window: %s", executable, windowtitle)
- if backend:
- self.set_windows_backend(backend)
-
- app = pywinauto.Application(backend=self._backend).start(
- cmd_line=executable, work_dir=work_dir
- )
- app_instance = self.open_dialog(windowtitle, wildcard=wildcard)
- self._apps[app_instance]["app"] = app
- self._apps[app_instance]["executable"] = executable
- self._apps[app_instance]["startkeyword"] = "Open Executable"
- return app_instance
-
- def open_using_run_dialog(
- self,
- executable: str,
- windowtitle: str,
- timeout: int = 10,
- wildcard: bool = False,
- ) -> int:
- """Open application using Windows run dialog.
- Window title name is required to get handle on the application.
-
- :param executable: name of the executable
- :param windowtitle: name of the window
- :param timeout: time to wait for dialog to appear
- :param wildcard: set True for inclusive window title search, default False
- :return: application instance id
-
- Example:
-
- .. code-block:: robotframework
-
- ${app1} Open Using Run Dialog notepad Untitled - Notepad
- ${app2} Open Using Run Dialog notepad Notepad wildcard=True
-
- """
- self.send_keys("{VK_LWIN down}r{VK_LWIN up}")
- delay(1)
-
- self.send_keys_to_input(executable, send_delay=0.2, enter_delay=0.5)
-
- app_instance = self.open_dialog(windowtitle, timeout=timeout, wildcard=wildcard)
- self._apps[app_instance]["executable"] = executable
- self._apps[app_instance]["startkeyword"] = "Open Using Run Dialog"
- return app_instance
-
- def open_from_search(
- self,
- executable: str,
- windowtitle: str,
- timeout: int = 10,
- wildcard: bool = False,
- ) -> int:
- """Open application using Windows search dialog.
- Window title name is required to get handle on the application.
-
- :param executable: name of the executable
- :param windowtitle: name of the window
- :param timeout: time to wait for dialog to appear
- :param wildcard: set True for inclusive window title search, default False
- :return: application instance id
-
- Example:
-
- .. code-block:: robotframework
-
- ${app1} Open From Search calculator Calculator
- ${app2} Open From Search notepad Notepad wildcard=True
-
- """
- self.logger.info("Run from start menu: %s", executable)
- self.send_keys("{LWIN}")
- delay(1)
-
- self.send_keys_to_input(executable)
-
- app_instance = None
- for _ in range(10):
- try:
- app_instance = self.open_dialog(
- windowtitle, timeout=timeout, wildcard=wildcard
- )
- except AttributeError:
- pass
- except COMError:
- pass
- if app_instance:
- self._apps[app_instance]["executable"] = executable
- self._apps[app_instance]["startkeyword"] = "Open From Search"
- break
- else:
- time.sleep(0.5)
- if not app_instance:
- raise ApplicationNotStarted("Unable to get application instance")
- return app_instance
-
- def get_spaced_string(self, text):
- """Replace spaces in a text with `pywinauto.keyboard`
- space characters `{VK_SPACE}`
-
- :param text: replace spaces in this string
-
- Example:
-
- .. code-block:: robotframework
-
- ${txt} Get Spaced String My name is Bond
- # ${txt} = My{VK_SPACE}name{VK_SPACE}is{VK_SPACE}Bond
- Send Keys To Input ${txt}
-
- """
- return text.replace(" ", "{VK_SPACE}")
-
- def send_keys_to_input(
- self,
- keys_to_type: str,
- with_enter: bool = True,
- send_delay: float = 0.5,
- enter_delay: float = 1.5,
- ) -> None:
- """Send keys to windows and add ENTER if `with_enter` is True
-
- At the end of send_keys there is by default 0.5 second delay.
- At the end of ENTER there is by default 1.5 second delay.
-
- :param keys_to_type: keys to type into Windows
- :param with_enter: send ENTER if `with_enter` is True
- :param send_delay: delay after send_keys
- :param enter_delay: delay after ENTER
-
- Example:
-
- .. code-block:: robotframework
-
- ${txt} Get Spaced String My name is Bond, James Bond
- Send Keys To Input ${txt} with_enter=False
- Send Keys To Input {ENTER}THE send_delay=5.0 with_enter=False
- Send Keys To Input {VK_SPACE}-{VK_SPACE}END enter_delay=5.0
-
- """
- # Set keyboard layout for Windows platform
- if platform.system() == "Windows":
- win32api.LoadKeyboardLayout("00000409", 1)
-
- self.send_keys(keys_to_type)
- delay(send_delay)
- if with_enter:
- self.send_keys("{ENTER}")
- delay(enter_delay)
-
- def minimize_dialog(self, windowtitle: str = None) -> None:
- """Minimize window by its title
-
- :param windowtitle: name of the window, default `None` means that
- active window is going to be minimized
-
- Example:
-
- .. code-block:: robotframework
-
- Open Using Run Dialog calc Calculator
- Open Using Run Dialog notepad Untitled - Notepad
- Minimize Dialog # Current window (Notepad)
- Minimize Dialog Calculator
-
- """
- windowtitle = (
- windowtitle or self._apps[self._active_app_instance]["windowtitle"]
- )
- self.logger.info("Minimize dialog: %s", windowtitle)
- self.dlg = pywinauto.Desktop(backend=self._backend)[windowtitle]
- self.dlg.minimize()
-
- def restore_dialog(self, windowtitle: str = None) -> None: # noqa: C901
- """Restore window by its title
-
- :param windowtitle: name of the window, default `None` means that
- active window is going to be restored
-
- Example:
-
- .. code-block:: robotframework
-
- Open Using Run Dialog notepad Untitled - Notepad
- Minimize Dialog
- Sleep 1s
- Restore Dialog
- Sleep 1s
- Restore Dialog Untitled - Notepad
-
- """
- # TODO. Handle too compled method
- self.logger.info("Restore dialog: %s", windowtitle)
- if windowtitle is None and self._active_app_instance == -1:
- raise ValueError(
- "There are no applications opened by library or window title is empty"
- )
- app = None
- handle = None
- if windowtitle is None:
- app = self._apps[self._active_app_instance]["app"]
- handle = self._apps[self._active_app_instance]["handle"]
- windowtitle = self._apps[self._active_app_instance]["windowtitle"]
- else:
- for app_id, app in self._apps.items():
- if "windowtitle" in app.keys() and windowtitle == app["windowtitle"]:
- application_id = app_id
- app = self._apps[application_id]["app"]
- handle = self._apps[application_id]["handle"]
- try:
- if app and handle > 0:
- app.window(handle=handle).restore()
- else:
- wins = self.get_window_list()
- for win in wins:
- if windowtitle == win["title"]:
- handle = win["handle"]
- break
- if handle and handle > 0:
- self.connect_by_handle(handle, windowtitle=windowtitle)
- self.restore_dialog()
- else:
- raise ValueError("Could not restore dialog: %s" % windowtitle)
- except pywinauto.findwindows.ElementAmbiguousError as e:
- self.logger.info("Could not restore dialog, %s", str(e))
-
- def open_dialog(
- self,
- windowtitle: str = None,
- highlight: bool = False,
- timeout: int = 10,
- existing_app: bool = False,
- wildcard: bool = False,
- parse_elements: bool = True,
- ) -> Any:
- """Open window by its title.
-
- :param windowtitle: name of the window, defaults to active window if None
- :param highlight: draw outline for window if True, default False
- :param timeout: time to wait for dialog to appear
- :param existing_app: set True if selecting window which library has already
- accessed, default False
- :param wildcard: set True for inclusive window title search, default False
- :param parse_elements: set False to not to parse elements of the window,
- default True
-
- Example:
-
- .. code-block:: robotframework
-
- Open Dialog Untitled - Notepad
- Open Dialog Untitled - Notepad highlight=True timeout=5
- Open Dialog Notepad wildcard=True
-
- """
- self.logger.info("Open dialog: '%s'", windowtitle)
-
- app_instance = None
- end_time = time.time() + float(timeout)
- while time.time() < end_time and app_instance is None:
- for window in self.get_window_list():
- if (not wildcard and windowtitle == window["title"]) or (
- wildcard and windowtitle in window["title"]
- ):
- self.windowtitle = window["title"]
- app_instance = self.connect_by_handle(
- window["handle"],
- windowtitle=self.windowtitle,
- existing_app=existing_app,
- parse_elements=parse_elements,
- )
- break
- time.sleep(0.1)
-
- if app_instance is None:
- raise ValueError(
- "No window with title '%s', wildcard: %s" % (windowtitle, wildcard)
- )
-
- if highlight:
- self.dlg.draw_outline()
-
- return app_instance
-
- def connect_by_pid(self, app_pid: str, windowtitle: str = None) -> Any:
- """Connect to application by its pid
-
- :param app_pid: process id of the application
- :param windowtitle: name of the window, defaults to active window if None
-
- Example:
-
- .. code-block:: robotframework
-
- ${appid} Connect By PID 3231
-
- """
- self.logger.info("Connect to application pid: %s", app_pid)
- window_list = self.get_window_list()
- for win in window_list:
- if win["pid"] == app_pid:
- if windowtitle is None or (windowtitle and windowtitle in win["title"]):
- self.logger.info(
- "PID:%s matched window title:%s", win["pid"], win["title"]
- )
- return self.connect_by_handle(win["handle"], windowtitle)
- return None
-
- def connect_by_handle(
- self,
- handle: int,
- windowtitle: str = None,
- existing_app: bool = False,
- parse_elements: bool = True,
- ) -> Any:
- """Connect to application by its handle
-
- :param handle: handle of the application
- :param windowtitle: name of the window, defaults to active window if None
- :param existing_app: set True if selecting window which library has already
- accessed, default False
- :param parse_elements: set False to not to parse elements of the window,
- default True
-
- Example:
-
- .. code-block:: robotframework
-
- ${appid} Connect By Handle 88112
-
- """
- self.logger.info("Connect to application handle: %s", handle)
- app_instance = None
- app = pywinauto.Application(backend=self._backend).connect(
- handle=handle, visible_only=False
- )
- self.dlg = app.window(handle=handle)
- self.dlg.set_focus()
- params = None
- if existing_app:
- for key in self._apps:
- if self._apps[key]["handle"] == handle:
- app_instance = key
- break
- else:
- if windowtitle is not None:
- params = {"windowtitle": windowtitle}
- app_instance = self._add_app_instance(app=app, params=params, dialog=False)
- if parse_elements:
- self.refresh_window()
- return app_instance
-
- def close_all_applications(self) -> None:
- """Close all applications
-
- Example:
-
- .. code-block:: robotframework
-
- Open Application Excel
- Open Application Word
- Open Executable notepad.exe Untitled - Notepad
- Close All Applications
-
- """
- self.logger.info("Closing all applications")
- self.logger.debug("Applications in memory: %d", len(self._apps))
- for aid in list(self._apps):
- self.quit_application(aid)
- del self._apps[aid]
-
- def quit_application(self, app_id: int = None, send_keys: bool = False) -> None:
- """Quit an application by application id or
- active application if `app_id` is None.
-
- :param app_id: application_id, defaults to None
- :param send_keys: if ALT+F4 should be used to quit, default False
-
- Example:
-
- .. code-block:: robotframework
-
- ${app1} Open Application Excel
- ${app2} Open Application Word
- Quit Application ${app1}
-
- """
- app = self.get_app(app_id)
- self.logger.info("Quit application: %s (%s)", app_id, app)
- if send_keys:
- self.logger.info("Quit by F4 shortcut")
- self.switch_to_application(app_id)
- self.send_keys("%{F4}")
- else:
- if app["dispatched"]:
- self.logger.info("Quit by app.Quit()")
- app["app"].Quit()
- else:
- if "process_id" in app and app["process_id"] > 0:
- # pylint: disable=E1101
- pid = app["process_id"]
- if self.process_id_exists(pid):
- self.logger.info("Quit by killing process id")
- self.kill_process_by_pid(pid)
- else:
- self.logger.info("Process pid '%s' did not exist anymore", pid)
- else:
- self.logger.info("Quit by app.kill()")
- app["app"].kill()
- self._active_app_instance = -1
-
- def type_keys(self, keys: str) -> None:
- """Type keys into active window element.
-
- :param keys: list of keys to type
-
- Example:
-
- .. code-block:: robotframework
-
- Open Executable notepad.exe Untitled - Notepad
- Type Keys My text
-
- """
- if self.dlg is None:
- raise ValueError("No dialog open")
- self.dlg.type_keys(keys)
-
- def type_into(self, locator: str, keys: str, empty_field: bool = False) -> None:
- """Type keys into element matched by given locator.
-
- :param locator: element locator
- :param keys: list of keys to type
- :param empty_field: if field should be emptied before typing, default False
-
- Example:
-
- .. code-block:: robotframework
-
- Open Executable calc.exe Calculator
- Type Into CalculatorResults 11
- Type Into CalculatorResults 22 empty_field=True
-
- """
- elements, _ = self.find_element(locator)
- if elements and len(elements) == 1:
- ctrl = elements[0]["control"]
- empty_method = "^a{BACKSPACE}" if empty_field else ""
- ctrl.type_keys(f"{empty_method}{keys}")
- else:
- raise ValueError(f"Could not find unique element for '{locator}'")
-
- def send_keys(self, keys: str) -> None:
- """Send keys into active windows.
-
- :param keys: list of keys to send
-
- Example:
-
- .. code-block:: robotframework
-
- Open Executable calc.exe Calculator
- Send Keys 2{+}3{=}
-
- """
- pywinauto.keyboard.send_keys(keys)
-
- def get_text(self, locator: str) -> dict:
- """Get text from element
-
- :param locator: element locator
-
- Example:
-
- .. code-block:: robotframework
-
- Open Using Run Dialog calc Calculator
- Type Into CalculatorResults 11
- Type Into CalculatorResults 55
- &{val} Get Text CalculatorResults
-
- """
- elements, _ = self.find_element(locator)
- element_text = {}
- if elements and len(elements) == 1:
- ctrl = elements[0]["control"]
- element_text["value"] = (
- str(ctrl.get_value()) if hasattr(ctrl, "get_value") else None
- )
- element_text["children_texts"] = (
- "".join(ctrl.children_texts())
- if hasattr(ctrl, "children_texts")
- else None
- )
- element_text["rich_text"] = (
- elements[0]["rich_text"] if "rich_text" in elements[0].keys() else ""
- )
- legacy = (
- ctrl.legacy_properties() if hasattr(ctrl, "legacy_properties") else None
- )
- element_text["legacy_value"] = str(legacy["Value"]) if legacy else None
- element_text["legacy_name"] = str(legacy["Name"]) if legacy else None
- return element_text
-
- def mouse_click(
- self,
- locator: str = None,
- x: int = 0,
- y: int = 0,
- off_x: int = 0,
- off_y: int = 0,
- image: str = None,
- method: str = "locator",
- ctype: str = "click",
- focus: str = "center",
- tolerance: Optional[int] = None,
- ) -> None:
- # pylint: disable=C0301
- """Mouse click `locator`, `coordinates`, or `image`
-
- When using method `locator`,`image` or `ocr` mouse is clicked by default at
- center coordinates.
-
- Click types are:
-
- - `click` normal left button mouse click
- - `double`
- - `right`
-
- :param locator: element locator on active window
- :param x: coordinate x on desktop
- :param y: coordinate y on desktop
- :param off_x: offset x (used for locator and image clicks)
- :param off_y: offset y (used for locator and image clicks)
- :param image: image to click on desktop
- :param method: one of the available methods to mouse click, default "locator"
- :param ctype: type of mouse click
- :param focus: default point for element click is 'center', can be set to 'topleft'
- to click top left corner of the element
- :param tolerance: image matching tolerance between 0 and 1
-
- Example:
-
- .. code-block:: robotframework
-
- Mouse Click method=coordinates 100 100
- Mouse Click CalculatorResults
- Mouse Click method=image image=myimage.png off_x=10 off_y=10 ctype=right
- Mouse Click method=image image=myimage.png tolerance=0.8
- ${elements} ${other}= Find Element class:Button
- FOR ${element} IN @{elements}
- Run Keyword If ${element}[visible] Mouse Click ${element}
- END
- Mouse Click id:TrickyCheckbox focus=topleft
-
- """ # noqa: E501
- self.logger.info("Mouse click: %s", locator)
-
- if method == "locator":
- target_element = None
- if isinstance(locator, dict):
- target_element = locator
- else:
- element, _ = self.find_element(locator)
- if element and len(element) == 1:
- target_element = element[0]
- else:
- raise ValueError(f"Could not find unique element for '{locator}'")
- if target_element is None:
- raise ValueError("Could not find unique element to click")
- if focus == "topleft":
- x, y, _, _ = self._get_element_coordinates(target_element)
- x += 2
- y += 2
- else:
- x, y = self.get_element_center(target_element)
- self.click_type(x + off_x, y + off_y, ctype)
- elif method == "coordinates":
- self.mouse_click_coords(x, y, ctype)
- elif method == "image":
- self.mouse_click_image(image, off_x, off_y, ctype, tolerance)
-
- def mouse_click_image(
- self,
- template: str,
- off_x: int = 0,
- off_y: int = 0,
- ctype: str = "click",
- tolerance: Optional[float] = None,
- ) -> None:
- """Click at template image on desktop
-
- :param image: image to click on desktop
- :param off_x: horizontal offset from top left corner to click on
- :param off_y: vertical offset from top left corner to click on
- :param ctype: type of mouse click
- :param tolerance: matching tolerance between 0 and 1
-
- Example:
-
- .. code-block:: robotframework
-
- Mouse Click image=myimage.png off_x=10 off_y=10 ctype=right
- Mouse Click image=myimage.png tolerance=0.8
-
- """
- confidence = tolerance * 100.0 if tolerance is not None else None
- locator = ImageLocator(template, confidence=confidence)
-
- match = Desktop().find_element(locator)
-
- target = match.center.move(off_x, off_y)
- self.click_type(target.x, target.y, ctype)
-
- def mouse_click_coords(
- self, x: int, y: int, ctype: str = "click", delay_time: float = None
- ) -> None:
- """Click at coordinates on desktop
-
- :param x: horizontal coordinate on the windows to click
- :param y: vertical coordinate on the windows to click
- :param ctype: click type "click", "right" or "double", defaults to "click"
- :param delay: delay in seconds after, default is no delay
-
- Example:
-
- .. code-block:: robotframework
-
- Mouse Click Coords x=450 y=100
- Mouse Click Coords x=300 y=300 ctype=right
- Mouse Click Coords x=450 y=100 delay=5.0
-
- """
- self.click_type(x, y, ctype)
- if delay_time:
- delay(delay_time)
-
- def get_element(
- self, locator: str, screenshot: bool = False, open_dialog: bool = True
- ) -> Any:
- """Get element by locator.
-
- :param locator: name of the locator
- :param screenshot: takes element screenshot if True, defaults to False
- :param open_dialog: True if dialog should be reopened, default to True
- :return: element if element was identified, else False
-
- Example:
-
- .. code-block:: robotframework
-
- ${element} Get Element CalculatorResults
- ${element} Get Element Result screenshot=True
-
- """
- self.logger.info("Get element: %s", locator)
- if open_dialog:
- self.open_dialog(self.windowtitle)
- self.dlg.wait("exists enabled visible ready")
-
- matching_elements, _ = self.find_element(locator)
-
- if len(matching_elements) == 0:
- self.logger.info(
- "Locator '%s' not found in '%s'.\n",
- locator,
- self.windowtitle,
- )
- elif len(matching_elements) == 1:
- element = matching_elements[0]
- if screenshot:
- self.screenshot(f"locator_{locator}", element=element)
- for key in element.keys():
- self.logger.debug("%s=%s", key, element[key])
- return element
- else:
- # TODO: return more valuable information about what should
- # be matching element ?
- self.logger.info(
- "Locator '%s' matched multiple elements in '%s'. ",
- locator,
- self.windowtitle,
- )
- return False
-
- def get_element_rich_text(self, locator: str) -> Any:
- """Get value of element `rich text` attribute.
-
- :param locator: element locator
- :return: `rich_text` value if found, else False
-
- Example:
-
- .. code-block:: robotframework
-
- ${text} Get Element Rich Text CalculatorResults
-
- """
- element = self.get_element(locator)
- if element is not False and "rich_text" in element:
- return element["rich_text"]
- elif element is False:
- self.logger.info("Did not find element with locator: %s", locator)
- return False
- else:
- self.logger.info(
- "Element for locator %s does not have 'rich_text' attribute", locator
- )
- return False
-
- def get_element_rectangle(self, locator: str, as_dict: bool = False) -> Any:
- # pylint: disable=C0301
- """Get value of element `rectangle` attribute.
-
- :param locator: element locator
- :param as_dict: return values in a dictionary, default `False`
- :return: (left, top, right, bottom) values if found, else False
-
- Example:
-
- .. code-block:: robotframework
-
- ${left} ${top} ${right} ${bottom}= Get Element Rectangle CalculatorResults
- &{coords} Get Element Rectangle CalculatorResults as_dict=True
- Log top=${coords.top} left=${coords.left}
-
- """ # noqa: E501
- rectangle = self._get_element_attribute(locator, "rectangle")
- left, top, right, bottom = self._get_element_coordinates(rectangle)
- if as_dict:
- return {"left": left, "top": top, "right": right, "bottom": bottom}
- return left, top, right, bottom
-
- def _get_element_attribute(self, locator: str, attribute: str) -> Any:
- element = self.get_element(locator)
- if element is not False and attribute in element:
- return element[attribute]
- elif element is False:
- self.logger.info("Did not find element with locator %s", locator)
- return False
- else:
- self.logger.info(
- "Element for locator %s does not have 'visible' attribute", locator
- )
- return False
-
- def is_element_visible(self, locator: str) -> bool:
- """Is element visible.
-
- :param locator: element locator
- :return: True if visible, else False
-
- Example:
-
- .. code-block:: robotframework
-
- ${res}= Is Element Visible CalculatorResults
-
- """
- visible = self._get_element_attribute(locator, "visible")
- return bool(visible)
-
- def is_element_enabled(self, locator: str) -> bool:
- """Is element enabled.
-
- :param locator: element locator
- :return: True if enabled, else False
-
- Example:
-
- .. code-block:: robotframework
-
- ${res}= Is Element Enabled CalculatorResults
-
- """
- enabled = self._get_element_attribute(locator, "enabled")
- return bool(enabled)
-
- def menu_select(self, menuitem: str) -> None:
- """Select item from menu
-
- :param menuitem: name of the menu item
-
- Example:
-
- .. code-block:: robotframework
-
- Open Using Run Dialog notepad Untitled - Notepad
- Menu Select File->Print
-
- """
- self.logger.info("Menu select: %s", menuitem)
- if self.dlg is None:
- raise ValueError("No dialog open")
- try:
- self.dlg.menu_select(menuitem)
- except AttributeError as e:
- raise MenuItemNotFoundError(
- "Unable to access menu item '%s'" % menuitem
- ) from e
-
- def wait_for_element(
- self,
- locator: str,
- use_refreshing: bool = False,
- search_criteria: str = None,
- timeout: float = 30.0,
- interval: float = 2.0,
- ) -> Any:
- """Wait for element to appear into the window.
-
- Can return 1 or more elements matching locator, or raises
- `ElementNotFoundError` if element is not found within timeout.
-
- :param locator: name of the locator
- :param use_refreshing: wait for element(s) which are not there yet e.g. listbox
- item or popups, default False
- :param search_criteria: criteria by which element is matched
- :param timeout: defines how long to wait for element to appear,
- defaults to 30.0 seconds
- :param interval: how often to poll for element,
- defaults to 2.0 seconds (minimum is 0.5 seconds)
-
- Example:
-
- .. code-block:: robotframework
-
- @{elements} Wait For Element CalculatorResults
- @{elements} Wait For Element Results timeout=10 interval=1.5
-
- """
- self.refresh_window()
- end_time = time.time() + float(timeout)
- interval = max([0.5, interval])
- elements = None
- while time.time() < end_time:
- elements, _ = self.find_element(locator, search_criteria)
- if use_refreshing:
- self.refresh_window()
- if len(elements) > 0:
- break
- if interval >= timeout:
- self.logger.info(
- "Wait For Element: interval has been set longer than timeout - "
- "executing one cycle."
- )
- break
- if time.time() >= end_time:
- break
- time.sleep(interval)
- if elements:
- return elements
- raise ElementNotFoundError
-
- def find_element(self, locator: str, search_criteria: str = None) -> Any:
- """Find element from window by locator and criteria.
-
- :param locator: name of the locator
- :param search_criteria: criteria by which element is matched
- :return: list of matching elements and locators that were found on the window
-
- Example:
-
- .. code-block:: robotframework
-
- @{elements} Find Element CalculatorResults
- Log Many ${elements[0]} # list of matching elements
- Log Many ${elements[1]} # list of all available locators
-
- """
- match_type, search_locators = self._parse_locator(locator, search_criteria)
- if self.elements is None:
- controls, elements = self.get_window_elements()
- else:
- controls = self.controls
- elements = self.elements
-
- matching_elements, locators = [], []
-
- for ctrl, element in zip(controls, elements):
- match_results = {}
- for criteria, search_locator in search_locators:
- match_results[criteria] = self.is_element_matching(
- element, search_locator, criteria
- )
- if (match_type == "all" and all(match_results.values())) or (
- match_type == "any" and any(match_results.values())
- ):
- element["control"] = ctrl
- matching_elements.append(element)
- return matching_elements, locators
-
- def _determine_search_criteria(self, locator: str) -> Any:
- """Check search criteria from locator.
-
- Possible search criterias:
- - name
- - class / class_name
- - type / control_type
- - id / automation_id
- - partial name (wildcard search for 'name' attribute)
- - any (if none was defined)
-
- :param locator: name of the locator
- :return: criteria and locator
- """
- if locator.startswith("name:"):
- search_criteria = "name"
- _, locator = locator.split(":", 1)
- elif locator.startswith(("class_name:", "class:")):
- search_criteria = "class_name"
- _, locator = locator.split(":", 1)
- elif locator.startswith(("control_type:", "type:")):
- search_criteria = "control_type"
- _, locator = locator.split(":", 1)
- elif locator.startswith(("automation_id:", "id:")):
- search_criteria = "automation_id"
- _, locator = locator.split(":", 1)
- elif locator.startswith("partial name:"):
- search_criteria = "partial name"
- _, locator = locator.split(":", 1)
- elif locator.startswith("regexp:"):
- search_criteria = "regexp"
- _, locator = locator.split(":", 1)
- else:
- search_criteria = "any"
-
- return search_criteria, locator
-
- # TODO: supporting multiple search criterias at same time to identify ONE element
- def _is_element_matching(
- self, itemdict: dict, locator: str, criteria: str, wildcard: bool = False
- ) -> bool:
- if criteria == "regexp":
- name_search = re.search(locator, itemdict["name"])
- class_search = re.search(locator, itemdict["class_name"])
- type_search = re.search(locator, itemdict["control_type"])
- id_search = re.search(locator, itemdict["automation_id"])
- return name_search or class_search or type_search or id_search
- elif criteria != "any" and criteria in itemdict:
- if (wildcard and locator in itemdict[criteria]) or (
- locator == itemdict[criteria]
- ):
- return True
- elif criteria == "any":
- name_search = self.is_element_matching(itemdict, locator, "name")
- class_search = self.is_element_matching(itemdict, locator, "class_name")
- type_search = self.is_element_matching(itemdict, locator, "control_type")
- id_search = self.is_element_matching(itemdict, locator, "automation_id")
- if name_search or class_search or type_search or id_search:
- return True
- elif criteria == "partial name":
- return self.is_element_matching(itemdict, locator, "name", True)
- return False
-
- # TODO: supporting multiple search criterias at same time to identify ONE element
- def is_element_matching(
- self, itemdict: dict, locator: str, criteria: str, wildcard: bool = False
- ) -> bool:
- """Is element matching. Check if locator is found in `any` field
- or `criteria` field in the window items.
-
- :param itemDict: dictionary of element items
- :param locator: name of the locator
- :param criteria: criteria on which to match element
- :param wildcard: whether to do reg exp match or not, default False
- :return: True if element is matching locator and criteria, False if not
- """
- return self._is_element_matching(itemdict, locator, criteria, wildcard)
-
- def get_dialog_rectangle(self, ctrl: Any = None, as_dict: bool = False) -> Any:
- """Get dialog rectangle coordinates
-
- If `ctrl` is None then get coordinates from `dialog`
-
- :param ctrl: name of the window control object, defaults to None
- :return: coordinates: left, top, right, bottom
-
- Example:
-
- .. code-block:: robotframework
-
- ${left} ${top} ${right} ${bottom}= Get Dialog Rectangle
- &{coords} Get Dialog Rectangle as_dict=True
- Log top=${coords.top} left=${coords.left}
-
- """
- if ctrl:
- rect = ctrl.element_info.rectangle
- elif self.dlg:
- rect = self.dlg.element_info.rectangle
- else:
- raise ValueError("No dialog open")
-
- if as_dict:
- return {
- "left": rect.left,
- "top": rect.top,
- "right": rect.right,
- "bottom": rect.bottom,
- }
- else:
- return rect.left, rect.top, rect.right, rect.bottom
-
- def get_element_center(self, element: dict) -> Any:
- """Get element center coordinates
-
- :param element: dictionary of element items
- :return: coordinates, x and y
-
- Example:
-
- .. code-block:: robotframework
-
- @{element} Find Element CalculatorResults
- ${x} ${y}= Get Element Center ${elements[0][0]}
-
- """
- return self.calculate_rectangle_center(element["rectangle"])
-
- def click_type(
- self, x: int = None, y: int = None, click_type: str = "click"
- ) -> None:
- """Mouse click on coordinates x and y.
-
- Default click type is `click` meaning `left`
-
- :param x: horizontal coordinate for click, defaults to None
- :param y: vertical coordinate for click, defaults to None
- :param click_type: "click", "right" or "double", defaults to "click"
- :raises ValueError: if coordinates are not valid
-
- Example:
-
- .. code-block:: robotframework
-
- Click Type x=450 y=100
- Click Type x=450 y=100 click_type=right
- Click Type x=450 y=100 click_type=double
-
- """
- self.logger.info("Click type '%s' at (%s, %s)", click_type, x, y)
- if (x is None and y is None) or (x < 0 or y < 0):
- raise ValueError(f"Can't click on given coordinates: ({x}, {y})")
- if click_type == "click":
- pywinauto.mouse.click(coords=(x, y))
- elif click_type == "double":
- pywinauto.mouse.double_click(coords=(x, y))
- elif click_type == "right":
- pywinauto.mouse.right_click(coords=(x, y))
-
- def get_window_elements(
- self,
- screenshot: bool = False,
- element_json: bool = False,
- outline: bool = False,
- ) -> Any:
- # pylint: disable=C0301
- """Get element information about all window dialog controls
- and their descendants.
-
- :param screenshot: save element screenshot if True, defaults to False
- :param element_json: save element json if True, defaults to False
- :param outline: highlight elements if True, defaults to False
- :return: all controls and all elements
-
- Example:
-
- .. code-block:: robotframework
-
- @{elements} Get Window Elements
- Log Many ${elements[0]} # list of all available locators
- Log Many ${elements[1]} # list of matching elements
- @{elements} Get Window Elements screenshot=True element_json=True outline=True
-
- """ # noqa: E501
- ctrls = self._get_all_window_controls()
- elements, controls = [], []
- for _, ctrl in enumerate(ctrls):
- try:
- if not hasattr(ctrl, "element_info"):
- continue
- except COMError as ce:
- self.logger.info("Got COM error: %s", str(ce))
- continue
-
- filename = clean_filename(
- f"locator_{self.windowtitle}_ctrl_{ctrl.element_info.name}"
- )
-
- if screenshot and len(ctrl.element_info.name) > 0:
- self.screenshot(filename, ctrl=ctrl, overwrite=True)
- if outline:
- ctrl.draw_outline(colour="red", thickness=4)
- delay(0.2)
- ctrl.draw_outline(colour=0x000000, thickness=4)
-
- element = self._parse_element_attributes(element=ctrl)
- if element_json:
- write_element_info_as_json(element, filename)
-
- controls.append(ctrl)
- elements.append(element)
-
- if element_json:
- write_element_info_as_json(
- elements, clean_filename(f"locator_{self.windowtitle}_all_elements")
- )
-
- return controls, elements
-
- def _get_all_window_controls(self):
- if self.dlg is None:
- raise ValueError("No dialog open")
-
- ctrls = [self.dlg]
- try:
- if hasattr(self.dlg, "descendants"):
- ctrls += self.dlg.descendants()
- except COMError as ce:
- self.logger.info("Got COM error: %s", str(ce))
- return ctrls
-
- def _get_element_coordinates(self, element: Any) -> Any:
- """Get element coordinates from pywinauto object.
-
- :param rectangle: item containing rectangle information
- :return: coordinates: left, top, right, bottom
- """
- left = 0
- top = 0
- right = 0
- bottom = 0
- if isinstance(element, pywinauto.win32structures.RECT):
- left = element.left
- top = element.top
- right = element.right
- bottom = element.bottom
- elif isinstance(element, dict) and "rectangle" not in element.keys():
- left = element.left
- top = element.top
- right = element.right
- bottom = element.bottom
- else:
- if isinstance(element, dict) and "rectangle" in element.keys():
- rectangle = element["rectangle"]
- else:
- rectangle = element
- left, top, right, bottom = map(
- int,
- re.match(
- r"\(L([-]?\d+).*T([-]?\d+).*R([-]?\d+).*B([-]?\d+)\)",
- str(rectangle),
- ).groups(),
- )
- return left, top, right, bottom
-
- def screenshot(
- self,
- filename: str,
- element: dict = None,
- ctrl: Any = None,
- desktop: bool = False,
- overwrite: bool = True,
- ) -> None:
- """Save screenshot into filename.
-
- :param filename: name of the file
- :param element: take element screenshot, defaults to None
- :param ctrl: take control screenshot, defaults to None
- :param desktop: take desktop screenshot if True, defaults to False
- :param overwrite: overwrite existing image (deprecated, always True)
-
- Example:
-
- .. code-block:: robotframework
-
- @{element} Find Element CalculatorResults
- Screenshot element.png ${elements[0][0]}
- Screenshot desktop.png desktop=True
- Screenshot desktop.png desktop=True overwrite=True
-
- """
- del overwrite # Always overwrite
-
- if desktop:
- region = None
- elif element:
- region = self._get_element_coordinates(element["rectangle"])
- elif ctrl:
- region = self.get_dialog_rectangle(ctrl)
- else:
- region = self.get_dialog_rectangle()
-
- if region is not None:
- region = Region(*region)
-
- Desktop().take_screenshot(path=filename, locator=region)
-
- def _parse_element_attributes(self, element: dict) -> dict:
- """Return filtered element dictionary for an element.
-
- :param element: should contain `element_info` attribute
- :return: dictionary containing element attributes
- """
- if element is None and "element_info" not in element:
- self.logger.warning(
- "%s is none or does not have element_info attribute", element
- )
- return None
-
- element_dict = self._prepare_element_dict(element)
- element_info = element.element_info
- element_attributes = [a for a in dir(element_info) if not a.startswith("_")]
-
- for attr in element_attributes:
- try:
- attr_value = getattr(element_info, attr)
- if attr == "parent":
- element_dict["parent"] = getattr(attr_value, "control_type", None)
- else:
- element_dict[attr] = (
- attr_value() if callable(attr_value) else str(attr_value)
- )
- except TypeError:
- pass
- except NotImplementedError:
- pass
- except COMError as ce:
- self.logger.info("Got COM error: %s", str(ce))
-
- return self._clean_element_dict(element_dict)
-
- def _prepare_element_dict(self, element):
- element_dict = {}
-
- element_dict["object"] = element
- try:
- element_dict["legacy"] = (
- element.legacy_properties()
- if hasattr(element, "legacy_properties")
- else None
- )
- except AttributeError:
- pass
- return element_dict
-
- def _clean_element_dict(self, element_dict):
- attributes_to_remove = [
- # "automation_id",
- "children",
- # "class_name",
- # "control_id",
- # "control_type",
- "descendants",
- "dump_window",
- "element",
- # "enabled",
- "filter_with_depth",
- "framework_id",
- "from_point",
- # "handle",
- "has_depth",
- "iter_children",
- "iter_descendants",
- # "name",
- # "parent",
- # "process_id",
- # "rectangle",
- # "rich_text",
- # "runtime_id",
- "set_cache_strategy",
- "top_from_point",
- # "visible",
- ]
-
- for attr in attributes_to_remove:
- element_dict.pop(attr, None)
-
- return element_dict
-
- def put_system_to_sleep(self) -> None:
- """Put Windows into sleep mode
-
- Example:
-
- .. code-block:: robotframework
-
- Put System To Sleep
-
- """
- access = win32security.TOKEN_ADJUST_PRIVILEGES | win32security.TOKEN_QUERY
- htoken = win32security.OpenProcessToken(win32api.GetCurrentProcess(), access)
- if htoken:
- priv_id = win32security.LookupPrivilegeValue(
- None, win32security.SE_SHUTDOWN_NAME
- )
- win32security.AdjustTokenPrivileges(
- htoken, 0, [(priv_id, win32security.SE_PRIVILEGE_ENABLED)]
- )
- ctypes.windll.powrprof.SetSuspendState(False, True, True)
- win32api.CloseHandle(htoken)
-
- def lock_screen(self) -> None:
- """Put windows into lock mode
-
- Example:
-
- .. code-block:: robotframework
-
- Lock Screen
- """
- ctypes.windll.User32.LockWorkStation()
-
- def log_in(self, username: str, password: str, domain: str = ".") -> str:
- """Log into Windows `domain` with `username` and `password`.
-
- :param username: name of the user
- :param password: password of the user
- :param domain: windows domain for the user, defaults to "."
- :return: handle
-
- Example:
-
- .. code-block:: robotframework
-
- Log In username=myname password=mypassword domain=company
- """
- return win32security.LogonUser(
- username,
- domain,
- password,
- win32con.LOGON32_LOGON_INTERACTIVE,
- win32con.LOGON32_PROVIDER_DEFAULT,
- )
-
- def _validate_target(self, target: dict, target_locator: str) -> Any:
- target_x = target_y = 0
- if target_locator is not None:
- self.switch_to_application(target["id"])
- target_elements, _ = self.find_element(target_locator)
- if len(target_elements) == 0:
- raise ValueError(
- ("Target element was not found by locator '%s'" % target_locator)
- )
- elif len(target_elements) > 1:
- raise ValueError(
- (
- "Target element matched more than 1 element (%d) "
- "by locator '%s'" % (len(target_elements), target_locator)
- )
- )
- target_x, target_y = self.calculate_rectangle_center(
- target_elements[0]["rectangle"]
- )
- else:
- target_x, target_y = self.calculate_rectangle_center(
- target["dlg"].rectangle()
- )
- return target_x, target_y
-
- def _select_elements_for_drag(
- self, src: dict, src_locator: str, origin="middle"
- ) -> Any:
- self.switch_to_application(src["id"])
- source_elements, _ = self.find_element(src_locator)
- if len(source_elements) == 0:
- raise ValueError(
- ("Source elements where not found by locator '%s'", src_locator)
- )
- selections = []
- source_min_left = 99999
- source_max_right = -1
- source_min_top = 99999
- source_max_bottom = -1
- for elem in source_elements:
- left, top, right, bottom = self._get_element_coordinates(elem["rectangle"])
- if left < source_min_left:
- source_min_left = left
- if right > source_max_right:
- source_max_right = right
- if top < source_min_top:
- source_min_top = top
- if bottom > source_max_bottom:
- source_max_bottom = bottom
- mid_x = int((right - left) / 2) + left
- mid_y = int((bottom - top) / 2) + top
- if origin == "middle":
- selections.append((mid_x, mid_y))
- elif origin == "topleft":
- selections.append((left, top))
- source_x = int((source_max_right - source_min_left) / 2) + source_min_left
- source_y = int((source_max_bottom - source_min_top) / 2) + source_min_top
- return selections, source_x, source_y
-
- def drag_and_drop(
- self,
- src: Any,
- target: Any,
- src_locator: str,
- target_locator: str = None,
- handle_ctrl_key: bool = False,
- drop_delay: float = 2.0,
- origin: str = "middle",
- ) -> None:
- # pylint: disable=C0301
- """Drag elements from source and drop them on target.
-
- Please note that if CTRL is not pressed down during drag and drop then
- operation is MOVE operation, on CTRL down the operation is COPY operation.
-
- There will be also overwrite notification if dropping over existing files.
-
- :param src: application object or instance id
- :param target: application object or instance id
- :param src_locator: elements to move
- :param handle_ctrl_key: True if keyword should press CTRL down dragging
- :param drop_delay: how many seconds to wait until releasing mouse drop,
- default 2.0
- :raises ValueError: on validation errors
-
- Example:
-
- .. code-block:: robotframework
-
- ${app1}= Open Using Run Dialog explorer.exe{VK_SPACE}C:\\workfiles\\movethese movethese
- ${app2}= Open Using Run Dialog wordpad.exe Document - WordPad
- Drag And Drop ${app1} ${app2} regexp:testfile_\\d.txt name:Rich Text Window handle_ctrl_key=${True}
- Drag And Drop ${app1} ${app1} regexp:testfile_\\d.txt name:subdir handle_ctrl_key=${True}
-
- """ # noqa : E501
- if isinstance(src, int):
- src = self.get_app(src)
- if isinstance(target, int):
- target = self.get_app(target)
-
- single_application = src["app"] == target["app"]
- selections, source_x, source_y = self._select_elements_for_drag(
- src, src_locator, origin
- )
- target_x, target_y = self._validate_target(target, target_locator)
-
- self.logger.info(
- "Dragging %d elements from (%d,%d) to (%d,%d)",
- len(selections),
- source_x,
- source_y,
- target_x,
- target_y,
- )
-
- try:
- if handle_ctrl_key:
- self.send_keys("{VK_LCONTROL down}")
- delay(0.2)
-
- # Select elements by mouse clicking
- if not single_application:
- self.restore_dialog(src["windowtitle"])
- for idx, selection in enumerate(selections):
- self.logger.debug("Selecting item %d by mouse_click", idx)
- # pywinauto.mouse.click(coords=(selection[0]+5, selection[1]+5))
- self.mouse_click_coords(selection[0] + 5, selection[1] + 5)
-
- # Start drag from the last item
- pywinauto.mouse.press(coords=(source_x, source_y))
- delay(0.5)
- if not single_application:
- self.restore_dialog(target["windowtitle"])
- pywinauto.mouse.move(coords=(target_x, target_y))
-
- self.logger.debug("Cursor position: %s", win32api.GetCursorPos())
- delay(drop_delay)
- self.mouse_click_coords(target_x, target_y)
- pywinauto.mouse.click(coords=(target_x, target_y))
-
- # if action_required:
- self.send_keys("{ENTER}")
- if handle_ctrl_key:
- self.send_keys("{VK_LCONTROL up}")
- delay(0.5)
- # Deselect elements by mouse clicking
- for selection in selections:
- self.logger.debug("Deselecting item by mouse_click")
- self.mouse_click_coords(selection[0] + 5, selection[1] + 5)
- finally:
- self.send_keys("{VK_LCONTROL up}")
-
- def calculate_rectangle_center(self, rectangle: Any) -> Any:
- """Calculate x and y center coordinates from rectangle.
-
- :param rectangle: element rectangle coordinates
- :return: x and y coordinates of rectangle center
-
- Example:
-
- .. code-block:: robotframework
-
- Open Using Run Dialog calc Calculator
- &{rect}= Get Element Rectangle CalculatorResults
- ${x} ${y}= Calculate Rectangle Center ${rect}
- """
- left, top, right, bottom = self._get_element_coordinates(rectangle)
- x = int((right - left) / 2) + left
- y = int((bottom - top) / 2) + top
- return x, y
-
- def get_window_list(self):
- """Get list of open windows
-
- Window dictionaries contain:
-
- - automation_id
- - control_id
- - title
- - pid
- - handle
- - is_active
- - keyboard_focus
- - rectangle
-
- :return: list of window dictionaries
-
- Example:
-
- .. code-block:: robotframework
-
- @{windows} Get Window List
- FOR ${window} IN @{windows}
- Log Many ${window}
- END
- """
- windows = pywinauto.Desktop(backend=self._backend).windows()
- window_list = []
- for w in windows:
- try:
- left, top, right, bottom = self._get_element_coordinates(w.rectangle())
- window_list.append(
- {
- "automation_id": w.automation_id(),
- "control_id": w.control_id(),
- "title": w.window_text(),
- "pid": w.process_id(),
- "handle": w.handle,
- "is_active": w.is_active(),
- "keyboard_focus": w.has_keyboard_focus(),
- "rectangle": [left, top, right, bottom],
- "object": w,
- }
- )
- except Exception as e: # pylint: disable=broad-except
- self.logger.debug(str(e))
- return window_list
-
- def refresh_window(self):
- """Get controls and elements for current windows.
-
- Should be called always when window content changes on
- Windows desktop.
-
- :return: controls (list) and elements (list)
- """
- self.logger.debug("Refresh window")
- controls, elements = self.get_window_elements()
- self.elements = elements
- self.controls = controls
- return controls, elements
-
- def _parse_locator(self, locator: str, search_criteria: str):
- regex = rf"({':|'.join(WINDOWS_LOCATOR_STRATEGIES.keys())}:|or|and)('{{1}}(.+)'{{1}}|(\S+))?" # noqa: E501
- parts = re.finditer(regex, locator, re.IGNORECASE)
-
- locators = []
- match_type = "all"
-
- for part in parts:
- groups = part.groups()
- if groups[0].lower() == "or":
- match_type = "any"
- elif groups[0].lower() == "and":
- pass
- else:
- strategy, _ = groups[0].split(":")
- value = groups[2] if groups[2] else groups[3]
- locators.append([WINDOWS_LOCATOR_STRATEGIES[strategy], value])
-
- # Add some strategies if there aren't other valid strategies
- if not locators:
- match_type = "any"
- locators.append(["name", locator])
- locators.append(["automation_id", locator])
- locators.append([search_criteria, locator])
- return match_type, locators
-
- def set_automation_speed(self, speed: Speed = Speed.DEFAULT):
- """Set global automation timings
-
- :param speed: possible values 'default', 'fast' or 'slow'
- """
- speed = to_speed(speed)
-
- if speed == Speed.DEFAULT:
- pywinauto.timings.Timings.defaults()
- elif speed == Speed.SLOW:
- pywinauto.timings.Timings.slow()
- elif speed == Speed.FAST:
- pywinauto.timings.Timings.fast()
diff --git a/packages/main/src/RPA/Desktop/__init__.py b/packages/main/src/RPA/Desktop/__init__.py
index e99af2f024..761329770a 100644
--- a/packages/main/src/RPA/Desktop/__init__.py
+++ b/packages/main/src/RPA/Desktop/__init__.py
@@ -48,7 +48,7 @@ class Desktop(DynamicCore):
- Taking screenshots
- Clipboard management
- .. warning:: Windows element selectors are not currently supported, and require the use of ``RPA.Desktop.Windows``
+ .. warning:: Windows element selectors are not currently supported, and require the use of ``RPA.Windows``
**Installation**
diff --git a/packages/main/src/RPA/Desktop/keywords/screen.py b/packages/main/src/RPA/Desktop/keywords/screen.py
index 39e78e23ac..af1708883d 100644
--- a/packages/main/src/RPA/Desktop/keywords/screen.py
+++ b/packages/main/src/RPA/Desktop/keywords/screen.py
@@ -168,22 +168,26 @@ def get_display_dimensions(self) -> Region:
return _monitor_to_region(sct.monitors[0])
@keyword
- def highlight_elements(self, locator: LocatorType):
- """Draw an outline around all matching elements."""
+ def highlight_elements(self, locator: LocatorType) -> List[Region]:
+ """Draw an outline around all matching elements, and return their regions."""
if not utils.is_windows():
raise NotImplementedError("Not supported on non-Windows platforms")
matches = self.ctx.find_elements(locator)
+ regions = []
for match in matches:
if isinstance(match, Region):
- _draw_outline(match)
+ region = match
elif isinstance(match, Point):
# TODO: Draw a circle instead?
region = Region(match.x - 5, match.y - 5, match.x + 5, match.y + 5)
- _draw_outline(region)
else:
raise TypeError(f"Unknown location type: {match}")
+ _draw_outline(region)
+ regions.append(region)
+
+ return regions
@keyword
def define_region(self, left: int, top: int, right: int, bottom: int) -> Region:
diff --git a/packages/main/tests/python/test_desktop_screen.py b/packages/main/tests/python/test_desktop_screen.py
new file mode 100644
index 0000000000..05fd476174
--- /dev/null
+++ b/packages/main/tests/python/test_desktop_screen.py
@@ -0,0 +1,37 @@
+"""Tests for RPA.Desktop.keywords.screen."""
+from unittest.mock import MagicMock, patch
+
+from RPA.core.geometry import Point, Region
+from RPA.Desktop.keywords.screen import ScreenKeywords
+
+
+def _make_keywords():
+ return ScreenKeywords(MagicMock())
+
+
+def test_highlight_elements_returns_regions():
+ keywords = _make_keywords()
+ region = Region(1, 2, 3, 4)
+ keywords.ctx.find_elements.return_value = [region]
+
+ with patch(
+ "RPA.Desktop.keywords.screen.utils.is_windows", return_value=True
+ ), patch("RPA.Desktop.keywords.screen._draw_outline") as draw_outline:
+ result = keywords.highlight_elements("locator")
+
+ assert result == [region]
+ draw_outline.assert_called_once_with(region)
+
+
+def test_highlight_elements_converts_points_to_regions():
+ keywords = _make_keywords()
+ point = Point(10, 20)
+ keywords.ctx.find_elements.return_value = [point]
+
+ with patch(
+ "RPA.Desktop.keywords.screen.utils.is_windows", return_value=True
+ ), patch("RPA.Desktop.keywords.screen._draw_outline") as draw_outline:
+ result = keywords.highlight_elements("locator")
+
+ assert result == [Region(5, 15, 15, 25)]
+ draw_outline.assert_called_once_with(Region(5, 15, 15, 25))
diff --git a/packages/main/uv.lock b/packages/main/uv.lock
index a20f796c6b..3b61487702 100644
--- a/packages/main/uv.lock
+++ b/packages/main/uv.lock
@@ -2119,7 +2119,7 @@ wheels = [
[[package]]
name = "rpaframework"
-version = "32.0.2"
+version = "33.0.0"
source = { editable = "." }
dependencies = [
{ name = "beautifulsoup4" },