Repository: qgindi/LibreAutomate Branch: master Commit: 7121bef44741 Files: 768 Total size: 10.8 MB Directory structure: gitextract__t3y79t8/ ├── .editorconfig ├── .gitattributes ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── Au/ │ ├── Api/ │ │ ├── Api.cs │ │ ├── Api^kernel32.cs │ │ ├── Api^user32.cs │ │ ├── Api_COM.cs │ │ ├── Api_UIA.cs │ │ ├── Api_const.cs │ │ ├── Api_public.cs │ │ ├── Cpp.cs │ │ └── WinRT.cs │ ├── Au.More/ │ │ ├── AppSingleInstance.cs │ │ ├── BufferedPaint.cs │ │ ├── CheckListDialog.cs │ │ ├── ComUtil.cs │ │ ├── Convert2.cs │ │ ├── DebugTraceListener.cs │ │ ├── Dpi.cs │ │ ├── FastBuffer.cs │ │ ├── FileOpenSaveDialog.cs │ │ ├── GdiTextRenderer.cs │ │ ├── Hash.cs │ │ ├── HelpUtil.cs │ │ ├── HttpServerSession.cs │ │ ├── IconImageCache.cs │ │ ├── ImageUtil.cs │ │ ├── KeyToTextConverter.cs │ │ ├── Math2.cs │ │ ├── MemoryBitmap.cs │ │ ├── MemoryUtil.cs │ │ ├── MenuItemInfo.cs │ │ ├── MouseCursor.cs │ │ ├── RecordingUtil.cs │ │ ├── ResourceUtil.cs │ │ ├── SecurityUtil.cs │ │ ├── WaitableTimer.cs │ │ ├── WinEventHook.cs │ │ ├── WindowsHook.cs │ │ └── WinformsControlNames.cs │ ├── Au.Types/ │ │ ├── ColorInt.cs │ │ ├── JSettings.cs │ │ ├── TreeBase.cs │ │ ├── common.cs │ │ ├── exceptions.cs │ │ ├── param types.cs │ │ ├── structs.cs │ │ └── unused/ │ │ └── AuClassless.cs │ ├── Au.cs │ ├── Au.csproj │ ├── Ext/ │ │ ├── Bitmap.Resize.cs │ │ ├── ExtMisc.cs │ │ ├── ExtWpf.cs │ │ └── ExtXml.cs │ ├── Files, data/ │ │ ├── ExplorerFolder.cs │ │ ├── FileSystemRedirection.cs │ │ ├── FileTree.cs │ │ ├── FileWatcher.cs │ │ ├── Pidl.cs │ │ ├── TempFile.cs │ │ ├── filesystem-types.cs │ │ ├── filesystem.cs │ │ ├── filesystem.more.cs │ │ ├── folders.cs │ │ ├── icon.cs │ │ ├── pathname.cs │ │ ├── shortcutFile.cs │ │ ├── sqlite.cs │ │ └── sqlite_api.cs │ ├── GUI/ │ │ ├── EnumUI.cs │ │ ├── dialog-static.cs │ │ ├── dialog-types.cs │ │ ├── dialog-x-obsolete.cs │ │ ├── dialog.cs │ │ ├── osd.cs │ │ ├── popupMenu/ │ │ │ ├── MTBase.cs │ │ │ ├── pm acc.cs │ │ │ ├── pm render.cs │ │ │ ├── pm types.cs │ │ │ └── popupMenu.cs │ │ ├── toolbar/ │ │ │ ├── tb acc.cs │ │ │ ├── tb dialog.cs │ │ │ ├── tb man.cs │ │ │ ├── tb render.cs │ │ │ ├── tb sat.cs │ │ │ ├── tb types.cs │ │ │ ├── tb util.cs │ │ │ └── toolbar.cs │ │ ├── trayIcon.cs │ │ ├── wpf-types.cs │ │ └── wpfBuilder.cs │ ├── Input/ │ │ ├── RegisteredHotkey.cs │ │ ├── clipboard.cs │ │ ├── clipboardData.cs │ │ ├── inputBlocker.cs │ │ ├── keys.cs │ │ ├── keys.more.cs │ │ ├── keys_static.cs │ │ ├── keys_types.cs │ │ ├── keys_util.cs │ │ ├── miscInfo.cs │ │ ├── mouse.cs │ │ └── mouse_types.cs │ ├── Internal/ │ │ ├── ActCtx_.cs │ │ ├── ArrayBuilder_.cs │ │ ├── AssemblyUtil_.cs │ │ ├── AttachThreadInput_.cs │ │ ├── Debug_.cs │ │ ├── GC_.cs │ │ ├── GDI misc.cs │ │ ├── Handle_.cs │ │ ├── ILReader.cs │ │ ├── IconString_.cs │ │ ├── Jit_.cs │ │ ├── LaDebugger_.cs │ │ ├── LineWriter_.cs │ │ ├── MiniProgram_.cs │ │ ├── NamespaceDoc.cs │ │ ├── NativeFont_.cs │ │ ├── NativeScrollbar_.cs │ │ ├── NativeThread_.cs │ │ ├── PostToThisThread_.cs │ │ ├── ProcessStarter_.cs │ │ ├── Ptr_.cs │ │ ├── Serializer_.cs │ │ ├── SharedMemory_.cs │ │ ├── StaTaskScheduler_.cs │ │ ├── StringBuilder_.cs │ │ ├── Util_.cs │ │ ├── misc_.cs │ │ └── tables.cs │ ├── Other/ │ │ ├── PrintServer.cs │ │ ├── ScriptEditor.cs │ │ ├── internet.cs │ │ ├── lastError.cs │ │ ├── opt.cs │ │ ├── print.cs │ │ ├── screen.cs │ │ ├── script+.cs │ │ └── script.cs │ ├── Resources/ │ │ ├── AssemblyInfo.cs │ │ └── red_cross_cursor.cur │ ├── String/ │ │ ├── ExtString.cs │ │ ├── SegParser.cs │ │ ├── StringUtil.cs │ │ ├── csvTable.cs │ │ ├── regexp.cs │ │ ├── regexp_ExtString.cs │ │ ├── regexp_types.cs │ │ └── wildcard.cs │ ├── System/ │ │ ├── CpuUsage.cs │ │ ├── ProcessMemory.cs │ │ ├── computer.cs │ │ ├── consoleProcess.cs │ │ ├── osVersion.cs │ │ ├── process.cs │ │ ├── run.cs │ │ ├── sound.cs │ │ └── uacInfo.cs │ ├── Time/ │ │ ├── WaitLoop.cs │ │ ├── perf.cs │ │ ├── timer.cs │ │ ├── timer2.cs │ │ ├── wait.cs │ │ └── wait_for.cs │ ├── Triggers/ │ │ ├── Trigger.cs │ │ ├── Triggers.cs │ │ ├── TriggersListWindow.cs │ │ ├── Triggers_actions.cs │ │ ├── Triggers_hooks.cs │ │ ├── Triggers_util.cs │ │ └── Types/ │ │ ├── t-autotext.cs │ │ ├── t-hotkey.cs │ │ ├── t-mouse.cs │ │ └── t-window.cs │ ├── UI objects/ │ │ ├── CaptureScreen.cs │ │ ├── OcrGoogleCloud.cs │ │ ├── OcrMicrosoftAzure.cs │ │ ├── OcrTesseract.cs │ │ ├── OcrWin10.cs │ │ ├── elm.cs │ │ ├── elmFinder.cs │ │ ├── elm_func.cs │ │ ├── elm_types.cs │ │ ├── ocr.cs │ │ ├── ocrFinder.cs │ │ ├── ocr_types.cs │ │ ├── uiimage.cs │ │ ├── uiimageFinder.cs │ │ └── uiimage_types.cs │ ├── resources/ │ │ └── global2.cs │ ├── wnd/ │ │ ├── WProp.cs │ │ ├── WTaskbarButton.cs │ │ ├── WndCopyData.cs │ │ ├── WndSavedRect.cs │ │ ├── WndUtil.cs │ │ ├── inactive/ │ │ │ └── desktop.cs │ │ ├── wnd.cs │ │ ├── wndChildFinder.cs │ │ ├── wndFinder.cs │ │ ├── wnd_child.cs │ │ ├── wnd_find.cs │ │ ├── wnd_fromxy.cs │ │ ├── wnd_get.cs │ │ ├── wnd_other.cs │ │ ├── wnd_private.cs │ │ └── wnd_wait.cs │ └── x/ │ └── NuGet.md ├── Au.AppHost/ │ ├── AppHost.cpp │ ├── Au.AppHost.vcxproj │ ├── Au.AppHost.vcxproj.filters │ ├── ResourceHacker.txt │ └── coreclrhost.h ├── Au.Controls/ │ ├── Au.Controls.cs │ ├── Au.Controls.csproj │ ├── KMenuCommands/ │ │ ├── KMenuCommands+.cs │ │ └── KMenuCommands.cs │ ├── KPanels/ │ │ ├── FlexStackPanel.cs │ │ ├── ILeaf.cs │ │ ├── KPanels.cs │ │ ├── _Floating.cs │ │ ├── _Node.cs │ │ ├── dock.cs │ │ ├── stack.cs │ │ └── tab.cs │ ├── KScintilla/ │ │ ├── KScintilla.cs │ │ ├── Sci API.cs │ │ ├── Sci adapter.cs │ │ ├── Sci loader.cs │ │ ├── Sci other.cs │ │ ├── Sci styles.cs │ │ ├── Sci text.cs │ │ ├── SciImages.cs │ │ ├── SciTags.cs │ │ ├── SciTextBuilder.cs │ │ └── other/ │ │ └── KSciInfoBox.cs │ ├── KTreeView/ │ │ ├── KTreeView.cs │ │ ├── tv-acc.cs │ │ ├── tv-hh.cs │ │ ├── tv-misc.cs │ │ ├── tv-render.cs │ │ └── tv-types.cs │ ├── Simple/ │ │ ├── KCheckBox.cs │ │ ├── KCheckDropdownBox.cs │ │ ├── KColorPicker.cs │ │ ├── KDateTime.cs │ │ ├── KDialogWindow.cs │ │ ├── KGroupBox.cs │ │ ├── KHotkeyControl.cs │ │ ├── KListBoxItemWithImage.cs │ │ ├── KPasswordBox.cs │ │ ├── KPopup.cs │ │ ├── KPopupListBox.cs │ │ ├── KScreenComboBox.cs │ │ ├── KTextBox.cs │ │ └── KWpfMenu.cs │ ├── Util, Api/ │ │ ├── HwndHostAccessibleBase_.cs │ │ ├── KApi.cs │ │ ├── KExtWpf.cs │ │ └── KImageUtil.cs │ └── resources/ │ ├── AssemblyInfo.cs │ ├── Generic.xaml │ └── XamlResources.cs ├── Au.Editor/ │ ├── App/ │ │ ├── App-resources.xaml │ │ ├── App.TrayIcon.cs │ │ ├── App.cs │ │ ├── AppSettings.cs │ │ ├── CommandLine.cs │ │ ├── DOptions.cs │ │ ├── MainWindow.cs │ │ └── Menus.cs │ ├── Au.Editor.cs │ ├── Au.Editor.csproj │ ├── Compiler/ │ │ ├── Compiler.cs │ │ ├── EditorExtension.cs │ │ ├── ErrBuilder.cs │ │ ├── MetaComments.cs │ │ ├── MetaReferences.cs │ │ ├── RecentTT.cs │ │ ├── Run task.cs │ │ ├── TestInternal.cs │ │ ├── XCompiled.cs │ │ ├── XPublish.cs │ │ └── util/ │ │ ├── CompilerUtil.cs │ │ └── Compiler_resources.cs │ ├── Debugger/ │ │ ├── PD-stack.cs │ │ ├── PD-variables.cs │ │ ├── PD._Debugger.cs │ │ ├── PD._MiRecord.cs │ │ ├── PanelBreakpoints.cs │ │ └── PanelDebug.cs │ ├── Default/ │ │ ├── Commands.xml │ │ ├── Layout.xml │ │ ├── Snippets.xml │ │ └── Themes/ │ │ ├── Material dark.csv │ │ ├── One Monokai dark.csv │ │ └── Visual Studio dark.csv │ ├── Edit/ │ │ ├── Ci-types.cs │ │ ├── CiAutocorrect.cs │ │ ├── CiCompletion.cs │ │ ├── CiErrors.cs │ │ ├── CiFind.cs │ │ ├── CiFindGo.cs │ │ ├── CiFolding.cs │ │ ├── CiGoTo.cs │ │ ├── CiPopupList.cs │ │ ├── CiPopupText.cs │ │ ├── CiProjects.cs │ │ ├── CiQuickInfo.cs │ │ ├── CiSignature.cs │ │ ├── CiSnippets.cs │ │ ├── CiStyling.cs │ │ ├── CiText.cs │ │ ├── CiTools.cs │ │ ├── CiUtil.cs │ │ ├── CiUtilExt.cs │ │ ├── CiWinapi.cs │ │ ├── CiWorkspace.cs │ │ ├── CodeExporter.cs │ │ ├── CodeInfo.cs │ │ ├── EditGoBack.cs │ │ ├── GenerateCode.cs │ │ ├── InsertCode.cs │ │ ├── ModifyCode.cs │ │ ├── PanelEdit.cs │ │ ├── Sci-DD.cs │ │ ├── Sci-TR.cs │ │ ├── Sci-images.cs │ │ ├── SciCode.cs │ │ ├── SciTheme.cs │ │ └── SciUndo.cs │ ├── Files/ │ │ ├── DProperties.cs │ │ ├── FileNode.cs │ │ ├── Files+.cs │ │ ├── FilesModel.cs │ │ ├── FilesView.cs │ │ ├── Git.cs │ │ ├── Save.cs │ │ ├── SyncWithFilesystem.cs │ │ └── WorkspaceState.cs │ ├── LibreAutomate.iss │ ├── Panels/ │ │ ├── PanelBookmarks.cs │ │ ├── PanelFiles.cs │ │ ├── PanelFind.cs │ │ ├── PanelFound.cs │ │ ├── PanelHelp.cs │ │ ├── PanelMouse.cs │ │ ├── PanelOpen.cs │ │ ├── PanelOutline.cs │ │ ├── PanelOutput.cs │ │ ├── PanelRead.cs │ │ ├── PanelTasks.cs │ │ └── Panels.cs │ ├── Properties/ │ │ └── launchSettings.json │ ├── Test.cs │ ├── Tools/ │ │ ├── CapturingWithHotkey.cs │ │ ├── ColorQuantizer.cs │ │ ├── DCustomize.cs │ │ ├── DCustomizeContextMenu.cs │ │ ├── DEnumFiles.cs │ │ ├── DIcons.cs │ │ ├── DInputRecorder.cs │ │ ├── DPortable.cs │ │ ├── DPwnd.cs │ │ ├── DSnippets.cs │ │ ├── DWinapi.cs │ │ ├── Delm.cs │ │ ├── Dnuget.cs │ │ ├── Docr.cs │ │ ├── Duiimage.cs │ │ ├── Dwnd.cs │ │ ├── InfoWindow.cs │ │ ├── KSciCodeBox.cs │ │ ├── KSciCodeBoxWnd.cs │ │ ├── KTextExpressionBox.cs │ │ ├── KeysWindow.cs │ │ ├── QuickCapture.cs │ │ ├── RegexWindow.cs │ │ ├── Scripting.cs │ │ ├── TUtil-main-process.cs │ │ ├── TUtil.cs │ │ ├── ToolProcess.cs │ │ └── WindowFindCodeFormatter.cs │ ├── Triggers and toolbars/ │ │ ├── DCommandline.cs │ │ ├── DSchedule.cs │ │ ├── TT-tb.cs │ │ ├── TT-triggers.cs │ │ ├── TT._CodeAnalysis.cs │ │ ├── TriggersAndToolbars.cs │ │ └── WinScheduler.cs │ ├── _prePostBuild.cs │ ├── resources/ │ │ ├── AssemblyInfo.cs │ │ ├── Au.manifest │ │ └── ci/ │ │ ├── Class.xaml │ │ ├── Constant.xaml │ │ ├── Delegate.xaml │ │ ├── Enum.xaml │ │ ├── EnumMember.xaml │ │ ├── Event.xaml │ │ ├── ExpandScope.xaml │ │ ├── ExtensionMethod.xaml │ │ ├── Field.xaml │ │ ├── GroupBy.xaml │ │ ├── Interface.xaml │ │ ├── Keyword.xaml │ │ ├── Label.xaml │ │ ├── LocalMethod.xaml │ │ ├── LocalVariable.xaml │ │ ├── Method.xaml │ │ ├── Namespace.xaml │ │ ├── Operator.xaml │ │ ├── OverlayAbstract.xaml │ │ ├── OverlayInternal.xaml │ │ ├── OverlayPrivate.xaml │ │ ├── OverlayProtected.xaml │ │ ├── OverlayStatic.xaml │ │ ├── Property.xaml │ │ ├── Region.xaml │ │ ├── Snippet.xaml │ │ ├── Structure.xaml │ │ └── TypeParameter.xaml │ ├── xAI/ │ │ ├── AI search.cs │ │ ├── AiModel.cs │ │ ├── McpServer.cs │ │ └── McpTools.cs │ ├── xMisc/ │ │ ├── EnvVarUpdater.cs │ │ ├── Pip.cs │ │ ├── PipIPC.cs │ │ ├── RegHotkeys.cs │ │ └── UacDragDrop.cs │ └── xUtil/ │ ├── Downloader.cs │ ├── Ed util shared.cs │ ├── Ed util.cs │ ├── EdExt.cs │ ├── EdIcons.cs │ ├── Libs/ │ │ ├── DiffMatchPatch.cs │ │ └── EnglishPorter2Stemmer.cs │ ├── MetaCommentsParser.cs │ ├── NugetDownloader.cs │ └── RegexParser.cs ├── Au.sln ├── Cookbook/ │ └── files.xml ├── Cpp/ │ ├── Cpp.cpp │ ├── Cpp.def │ ├── Cpp.h │ ├── Cpp.manifest │ ├── Cpp.rc │ ├── Cpp.vcxproj │ ├── Cpp.vcxproj.filters │ ├── IAccessible2.h │ ├── ISimpleDOMDocument.h │ ├── ISimpleDOMNode.h │ ├── ISimpleDOMText.h │ ├── JAB.h │ ├── MemoryPool.cpp │ ├── MemoryPool.h │ ├── Util.cpp │ ├── acc bridge.cpp │ ├── acc find.cpp │ ├── acc func.cpp │ ├── acc get.cpp │ ├── acc java.cpp │ ├── acc uia.cpp │ ├── acc web.cpp │ ├── acc workaround.cpp │ ├── acc.h │ ├── in-proc.cpp │ ├── internal.h │ ├── other.cpp │ ├── rejected.cpp │ ├── resource.h │ ├── stdafx.cpp │ ├── stdafx.h │ ├── str.cpp │ ├── str.h │ ├── test Uia.cpp │ ├── test.cpp │ └── util.h ├── LICENSE.txt ├── Libraries/ │ ├── PCRE/ │ │ ├── PCRE.vcxproj │ │ ├── PCRE.vcxproj.filters │ │ ├── config.h │ │ ├── pcre2.h │ │ ├── pcre2_auto_possess.c │ │ ├── pcre2_chartables.c │ │ ├── pcre2_chkdint.c │ │ ├── pcre2_compile.c │ │ ├── pcre2_compile.h │ │ ├── pcre2_compile_class.c │ │ ├── pcre2_config.c │ │ ├── pcre2_context.c │ │ ├── pcre2_error.c │ │ ├── pcre2_extuni.c │ │ ├── pcre2_find_bracket.c │ │ ├── pcre2_internal.h │ │ ├── pcre2_intmodedep.h │ │ ├── pcre2_maketables.c │ │ ├── pcre2_match.c │ │ ├── pcre2_match_data.c │ │ ├── pcre2_newline.c │ │ ├── pcre2_ord2utf.c │ │ ├── pcre2_pattern_info.c │ │ ├── pcre2_script_run.c │ │ ├── pcre2_serialize.c │ │ ├── pcre2_string_utils.c │ │ ├── pcre2_study.c │ │ ├── pcre2_substring.c │ │ ├── pcre2_tables.c │ │ ├── pcre2_ucd.c │ │ ├── pcre2_ucp.h │ │ ├── pcre2_ucptables.c │ │ ├── pcre2_util.h │ │ ├── pcre2_valid_utf.c │ │ └── pcre2_xclass.c │ └── scintilla/ │ ├── .editorconfig │ ├── include/ │ │ ├── ILexer.h │ │ ├── ILoader.h │ │ ├── Sci_Position.h │ │ ├── Scintilla.iface │ │ ├── ScintillaCall.h │ │ ├── ScintillaMessages.h │ │ ├── ScintillaStructures.h │ │ ├── ScintillaTypes.h │ │ ├── ScintillaWidget.h │ │ └── scintilla.h │ ├── src/ │ │ ├── AutoComplete.cxx │ │ ├── AutoComplete.h │ │ ├── CallTip.cxx │ │ ├── CallTip.h │ │ ├── CaseConvert.cxx │ │ ├── CaseConvert.h │ │ ├── CaseFolder.cxx │ │ ├── CaseFolder.h │ │ ├── CellBuffer.cxx │ │ ├── CellBuffer.h │ │ ├── ChangeHistory.cxx │ │ ├── ChangeHistory.h │ │ ├── CharClassify.cxx │ │ ├── CharClassify.h │ │ ├── CharacterCategoryMap.cxx │ │ ├── CharacterCategoryMap.h │ │ ├── CharacterType.cxx │ │ ├── CharacterType.h │ │ ├── ContractionState.cxx │ │ ├── ContractionState.h │ │ ├── DBCS.cxx │ │ ├── DBCS.h │ │ ├── Debugging.h │ │ ├── Decoration.cxx │ │ ├── Decoration.h │ │ ├── Document.cxx │ │ ├── Document.h │ │ ├── EditModel.cxx │ │ ├── EditModel.h │ │ ├── EditView.cxx │ │ ├── EditView.h │ │ ├── Editor.cxx │ │ ├── Editor.h │ │ ├── ElapsedPeriod.h │ │ ├── Geometry.cxx │ │ ├── Geometry.h │ │ ├── Indicator.cxx │ │ ├── Indicator.h │ │ ├── KeyMap.cxx │ │ ├── KeyMap.h │ │ ├── LineMarker.cxx │ │ ├── LineMarker.h │ │ ├── MarginView.cxx │ │ ├── MarginView.h │ │ ├── Partitioning.h │ │ ├── PerLine.cxx │ │ ├── PerLine.h │ │ ├── Platform.h │ │ ├── Position.h │ │ ├── PositionCache.cxx │ │ ├── PositionCache.h │ │ ├── RESearch.cxx │ │ ├── RESearch.h │ │ ├── RunStyles.cxx │ │ ├── RunStyles.h │ │ ├── ScintillaBase.cxx │ │ ├── ScintillaBase.h │ │ ├── Selection.cxx │ │ ├── Selection.h │ │ ├── SparseVector.h │ │ ├── SplitVector.h │ │ ├── Style.cxx │ │ ├── Style.h │ │ ├── UndoHistory.cxx │ │ ├── UndoHistory.h │ │ ├── UniConversion.cxx │ │ ├── UniConversion.h │ │ ├── UniqueString.cxx │ │ ├── UniqueString.h │ │ ├── ViewStyle.cxx │ │ ├── ViewStyle.h │ │ ├── XPM.cxx │ │ └── XPM.h │ ├── version.txt │ └── win32/ │ ├── HanjaDic.cxx │ ├── HanjaDic.h │ ├── ListBox.cxx │ ├── ListBox.h │ ├── PlatWin.cxx │ ├── PlatWin.h │ ├── ScintRes.rc │ ├── Scintilla.def │ ├── Scintilla.vcxproj │ ├── ScintillaDLL.cxx │ ├── ScintillaWin.cxx │ ├── ScintillaWin.h │ ├── SurfaceD2D.cxx │ ├── SurfaceD2D.h │ ├── SurfaceGDI.cxx │ ├── SurfaceGDI.h │ └── WinTypes.h ├── Notes.txt ├── Other/ │ ├── Au.DllHost/ │ │ ├── Au.DllHost.vcxproj │ │ ├── Au.DllHost.vcxproj.filters │ │ └── DllHost.c │ ├── Au.Net4/ │ │ ├── App.config │ │ ├── Au.Net4.csproj │ │ ├── Net4.cs │ │ ├── Properties/ │ │ │ └── AssemblyInfo.cs │ │ └── TypelibConverter.cs │ ├── BuildEvents/ │ │ ├── Au.TestInternal.cs │ │ ├── BuildEvents.cs │ │ ├── BuildEvents.csproj │ │ ├── GitBinaryFiles.cs │ │ ├── Readme - Roslyn.txt │ │ └── Sftp.cs │ ├── DatabasesEtc/ │ │ ├── DatabasesEtc.csproj │ │ ├── Icons.cs │ │ ├── Program.cs │ │ ├── RefAndDoc.cs │ │ └── RefTxt.cs │ └── DocFX/ │ ├── DocFX.csproj │ └── _doc/ │ ├── .gitignore │ ├── api/ │ │ ├── .gitignore │ │ └── index.md │ ├── articles/ │ │ ├── Caller info parameter.md │ │ ├── Key names and operators.md │ │ ├── Library.md │ │ ├── Output tags.md │ │ ├── UAC.md │ │ ├── UI element issues.md │ │ ├── Wait timeout.md │ │ ├── Wildcard expression.md │ │ ├── index.md │ │ └── toc.yml │ ├── changes/ │ │ ├── future.md │ │ ├── old/ │ │ │ ├── v0.1.md │ │ │ ├── v0.10.md │ │ │ ├── v0.11.md │ │ │ ├── v0.12.md │ │ │ ├── v0.13.md │ │ │ ├── v0.14.md │ │ │ ├── v0.15.md │ │ │ ├── v0.16.md │ │ │ ├── v0.17.md │ │ │ ├── v0.18.md │ │ │ ├── v0.19.md │ │ │ ├── v0.2.md │ │ │ ├── v0.3.md │ │ │ ├── v0.4.md │ │ │ ├── v0.5.md │ │ │ ├── v0.6.md │ │ │ ├── v0.7.md │ │ │ ├── v0.8.md │ │ │ ├── v0.9.md │ │ │ ├── v1.0.md │ │ │ ├── v1.1.md │ │ │ ├── v1.2.md │ │ │ ├── v1.3.md │ │ │ ├── v1.4.md │ │ │ ├── v1.5.md │ │ │ ├── v1.6.md │ │ │ ├── v1.7.md │ │ │ ├── v1.8.md │ │ │ └── v1.9.md │ │ ├── template.md │ │ ├── v1.10.md │ │ ├── v1.11.md │ │ ├── v1.12.md │ │ ├── v1.13.md │ │ ├── v1.14.md │ │ └── v1.15.md │ ├── docfx.json │ ├── editor/ │ │ ├── Application.md │ │ ├── Class files, projects.md │ │ ├── Code editor.md │ │ ├── Command line.md │ │ ├── Compared with QM.md │ │ ├── Creating exe programs.md │ │ ├── Debugger.md │ │ ├── File properties.md │ │ ├── Git, backup, sync.md │ │ ├── Icons.md │ │ ├── LA and AI.md │ │ ├── Menu commands.md │ │ ├── NuGet.md │ │ ├── PiP session.md │ │ ├── Portable app.md │ │ ├── Scripts.md │ │ ├── Settings.md │ │ ├── Snippets.md │ │ └── toc.yml │ ├── filter.yml │ ├── index.md │ ├── md-styles.css │ ├── misc/ │ │ └── privacy-policy.md │ ├── template1/ │ │ ├── layout/ │ │ │ └── _master.tmpl │ │ ├── mod.txt │ │ ├── partials/ │ │ │ ├── class.header.tmpl.partial │ │ │ ├── footer.tmpl.partial │ │ │ ├── logo.tmpl.partial │ │ │ ├── namespace.tmpl.partial │ │ │ └── navbar.tmpl.partial │ │ └── styles/ │ │ ├── main.css │ │ └── main.js │ ├── template2/ │ │ └── partials/ │ │ ├── collection.tmpl.partial │ │ └── item.tmpl.partial │ └── toc.yml ├── README.md ├── Scripts/ │ ├── @Au docs/ │ │ ├── Au docs.cs │ │ ├── AuDocs analyze.cs │ │ ├── AuDocs cookbook.cs │ │ ├── AuDocs other tasks.cs │ │ ├── AuDocs text.cs │ │ ├── AuDocs.cs │ │ ├── LA docs doc-html.db.cs │ │ ├── LA docs toc.json.cs │ │ ├── LA menu doc.cs │ │ └── Readme.txt │ ├── @WinAPI converter/ │ │ ├── WinAPI converter.cs │ │ ├── WinApiConverter+.cs │ │ └── WinApiConverter.cs │ ├── AuDocsLib.cs │ ├── Create NuGet package.cs │ ├── LA docs for AI/ │ │ ├── AI summaries.cs │ │ └── Upload AI embeddings.cs │ ├── Minimal .NET SDK.cs │ ├── Update version.txt.cs │ └── old/ │ ├── VS goto.cs │ └── Windows SDK to C#/ │ ├── @SDK converter/ │ │ ├── Classes.cs │ │ ├── Constants.cs │ │ ├── Converter.cs │ │ ├── Expr.cs │ │ ├── Functions.cs │ │ ├── Parameters.cs │ │ ├── SDK converter.cs │ │ ├── Tokenize.cs │ │ ├── Types.cs │ │ └── Util.cs │ ├── Readme.txt │ └── Scripts/ │ ├── SDK append 32-bit diff.cs │ ├── SDK create database.cs │ ├── SDK headers.h │ ├── SDK preprocessor.cs │ ├── SdkUtil.cs │ └── once/ │ ├── SDK get GUID.cs │ └── SDK get dll names.cs ├── _/ │ ├── default.exe.manifest │ ├── dotnet_ref_editor.txt │ ├── dotnet_ref_task.txt │ └── gitBinaryRestore.csv └── global.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ [*.{cs,vb}] # IDE0032: Use auto property dotnet_style_prefer_auto_properties = false:silent # CA1416: Validate platform compatibility dotnet_diagnostic.CA1416.severity = none # CA1806: Do not ignore method results dotnet_diagnostic.CA1806.severity = silent # CA2014: Do not use stackalloc in loops dotnet_diagnostic.CA2014.severity = warning # CS1573: Parameter has no matching param tag in the XML comment (but other parameters do) dotnet_diagnostic.CS1573.severity = suggestion [*] charset = utf-8 [*.iss] charset = utf-8-bom ================================================ FILE: .gitattributes ================================================ ############################################################################### # Set default behavior to automatically normalize line endings. ############################################################################### * text=auto ############################################################################### # Set the merge driver for project and solution files # # Merging from the command prompt will add diff markers to the files if there # are conflicts (Merging from VS is not affected by the settings below, in VS # the diff markers are never inserted). Diff markers may cause the following # file extensions to fail to load in VS. An alternative would be to treat # these files as binary and thus will always conflict and require user # intervention with every merge. To do so, just uncomment the entries below ############################################################################### #*.sln merge=binary #*.csproj merge=binary #*.vbproj merge=binary #*.vcxproj merge=binary #*.vcproj merge=binary #*.dbproj merge=binary #*.fsproj merge=binary #*.lsproj merge=binary #*.wixproj merge=binary #*.modelproj merge=binary #*.sqlproj merge=binary #*.wwaproj merge=binary ############################################################################### # behavior for image files # # image files are treated as binary by default. ############################################################################### #*.jpg binary #*.png binary #*.gif binary ############################################################################### /Libraries/** linguist-vendored ############################################################################### ================================================ FILE: .github/FUNDING.yml ================================================ github: qgindi ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. # User-specific files *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ build/ bld/ [Bb]in/ [Oo]bj/ # Visual Studio 2015 cache/options directory .vs/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # DNX project.lock.json artifacts/ *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opensdf *.sdf *.cachefile # Visual Studio profiler *.psess *.vsp *.vspx # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # NCrunch _NCrunch_* .*crunch*.local.xml # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml ## _TODO: Comment the next line if you want to checkin your ## web deploy settings but do note that will include unencrypted ## passwords #*.pubxml *.publishproj # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # Windows Azure Build Output csx/ *.build.csdef # Windows Store app package directory AppPackages/ # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # Others ClientBin/ [Ss]tyle[Cc]op.* ~$* *~ *.dbmdl *.dbproj.schemaview *.pfx *.snk *.publishsettings node_modules/ orleans.codegen.cs # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files *.mdf *.ldf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings # Microsoft Fakes FakesAssemblies/ # Node.js Tools for Visual Studio .ntvs_analysis.dat # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # LightSwitch generated files GeneratedArtifacts/ _Pvt_Extensions/ ModelManifest.xml # Au /Other/Api/ /Libraries/PCRE/excluded/ /_/* !/_/Default/ !/_/Templates/ !/_/*.manifest !/_/dotnet_ref_*.txt !/_/gitBinaryRestore.csv !/_/32/ /_/32/* !/_/32/7za.exe /_/Default/* !/_/Default/Workspace/ /_/Default/Workspace/* !/_/Default/Workspace/files/ !/_/Default/Workspace/files.xml !/_/Default/Workspace/state.db /_/Templates/* !/_/Templates/files/ !/_/Templates/files.xml /Cookbook/* !/Cookbook/files/ !/Cookbook/files.xml *.bak /Au/global.json .tools/ /Other/DocFX/_doc/cookbook /Au.Editor/Panels/PanelRead-WB.cs ================================================ FILE: Au/Api/Api.cs ================================================ //[assembly: DefaultDllImportSearchPaths(DllImportSearchPath.System32|DllImportSearchPath.UserDirectories)] #pragma warning disable 649, 169 //field never assigned/used namespace Au.Types; //[DebuggerStepThrough] static unsafe partial class Api { #region util /// /// Gets the native size of a struct variable. /// Returns Marshal.SizeOf(typeof(T)). /// Speed: the same (in Release config) as Marshal.SizeOf(typeof(T)), and 2 times faster than Marshal.SizeOf(v). /// internal static int SizeOf(T v) => Marshal.SizeOf(); /// /// Gets the native size of a type. /// Returns Marshal.SizeOf(typeof(T)). /// internal static int SizeOf() => Marshal.SizeOf(); /// /// Gets dll module handle (GetModuleHandle) or loads dll (NativeLibrary.TryLoad), and returns unmanaged exported function address (GetProcAddress). /// See also: GetDelegate. /// internal static IntPtr GetProcAddress(string dllName, string funcName) { IntPtr hmod = GetModuleHandle(dllName); if (hmod == default && !NativeLibrary.TryLoad(dllName, out hmod)) return default; return GetProcAddress(hmod, funcName); } /// /// Calls (loads dll or gets handle) and . /// internal static bool GetDelegate(out T deleg, string dllName, string funcName) where T : class { IntPtr fa = GetProcAddress(dllName, funcName); if (fa == default) { deleg = null; return false; } deleg = Marshal.GetDelegateForFunctionPointer(fa); return deleg != null; } /// /// Calls API and . /// internal static bool GetDelegate(out T deleg, IntPtr hModule, string funcName) where T : class { deleg = null; IntPtr fa = GetProcAddress(hModule, funcName); if (fa == default) return false; deleg = Marshal.GetDelegateForFunctionPointer(fa); return deleg != null; } /// /// If o is not null, calls . /// internal static void ReleaseComObject(T o) where T : class { if (o != null) Marshal.ReleaseComObject(o); } #endregion #region gdi32 [DllImport("gdi32.dll")] //this and many other GDI functions don't use SetLastError internal static extern bool DeleteObject(IntPtr ho); [DllImport("gdi32.dll")] internal static extern IntPtr CreateRectRgn(int x1, int y1, int x2, int y2); internal const int RGN_AND = 1; internal const int RGN_OR = 2; internal const int RGN_XOR = 3; internal const int RGN_DIFF = 4; internal const int RGN_COPY = 5; [DllImport("gdi32.dll")] internal static extern int CombineRgn(IntPtr hrgnDst, IntPtr hrgnSrc1, IntPtr hrgnSrc2, int iMode); [DllImport("gdi32.dll")] internal static extern bool SetRectRgn(IntPtr hrgn, int left, int top, int right, int bottom); [DllImport("gdi32.dll")] internal static extern IntPtr CreateRectRgnIndirect(in RECT lprect); [DllImport("gdi32.dll")] internal static extern bool PtInRegion(IntPtr hrgn, int x, int y); [DllImport("gdi32.dll")] internal static extern IntPtr CreateCompatibleDC(IntPtr hdc); [DllImport("gdi32.dll")] internal static extern bool DeleteDC(IntPtr hdc); [DllImport("gdi32.dll")] internal static extern IntPtr SelectObject(IntPtr hdc, IntPtr h); [DllImport("gdi32.dll", EntryPoint = "GetObjectW")] internal static extern int GetObject(IntPtr h, int c, void* pv); /// 1 transparent, 2 opaque. [DllImport("gdi32.dll")] internal static extern int SetBkMode(IntPtr hdc, int mode); [DllImport("gdi32.dll")] internal static extern int SetBkColor(IntPtr hdc, int color); [DllImport("gdi32.dll", EntryPoint = "TextOutW")] internal static extern bool TextOut(IntPtr hdc, int x, int y, string lpString, int c); [DllImport("gdi32.dll", EntryPoint = "TextOutW")] internal static extern bool TextOut(IntPtr hdc, int x, int y, char* lpString, int c); [DllImport("gdi32.dll", EntryPoint = "ExtTextOutW")] internal static extern bool ExtTextOut(IntPtr hdc, int x, int y, uint options, in RECT lprect, char* lpString, int c, int* lpDx = null); internal const uint ETO_CLIPPED = 0x4; [DllImport("gdi32.dll")] internal static extern bool MoveToEx(IntPtr hdc, int x, int y, out POINT lppt); [DllImport("gdi32.dll")] internal static extern bool GetCurrentPositionEx(IntPtr hdc, out POINT lppt); [DllImport("gdi32.dll")] internal static extern uint SetTextAlign(IntPtr hdc, uint align); [DllImport("gdi32.dll")] internal static extern int SetTextColor(IntPtr hdc, int color); [DllImport("gdi32.dll")] internal static extern IntPtr CreatePen(int iStyle, int cWidth, int color); [DllImport("gdi32.dll")] internal static extern bool LineTo(IntPtr hdc, int x, int y); [DllImport("gdi32.dll")] //tested: does not set last error internal static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int cx, int cy); [DllImport("gdi32.dll")] internal static extern int GetDeviceCaps(IntPtr hdc, int index); [DllImport("gdi32.dll", EntryPoint = "GetTextExtentPoint32W")] internal static extern bool GetTextExtentPoint32(IntPtr hdc, string lpString, int c, out SIZE psizl); [DllImport("gdi32.dll", EntryPoint = "GetTextExtentPoint32W")] internal static extern bool GetTextExtentPoint32(IntPtr hdc, char* lpString, int c, out SIZE psizl); [DllImport("gdi32.dll", EntryPoint = "CreateFontW")] internal static extern IntPtr CreateFont(int cHeight, int cWidth = 0, int cEscapement = 0, int cOrientation = 0, int cWeight = 0, int bItalic = 0, int bUnderline = 0, int bStrikeOut = 0, int iCharSet = 0, int iOutPrecision = 0, int iClipPrecision = 0, int iQuality = 0, int iPitchAndFamily = 0, string pszFaceName = null); [DllImport("gdi32.dll", EntryPoint = "CreateFontIndirectW")] internal static extern IntPtr CreateFontIndirect(in LOGFONT lplf); internal const uint SRCCOPY = 0xCC0020; internal const uint CAPTUREBLT = 0x40000000; [DllImport("gdi32.dll")] //tested: in some cases does not set last error even if returns false internal static extern bool BitBlt(IntPtr hdc, int x, int y, int cx, int cy, IntPtr hdcSrc, int x1, int y1, uint rop); [DllImport("gdi32.dll")] internal static extern bool StretchBlt(IntPtr hdcDest, int xDest, int yDest, int wDest, int hDest, IntPtr hdcSrc, int xSrc, int ySrc, int wSrc, int hSrc, uint rop); [DllImport("gdi32.dll")] internal static extern IntPtr CreateDIBSection(IntPtr hdc, in Api.BITMAPINFO pbmi, uint usage, out uint* ppvBits, IntPtr hSection = default, uint offset = 0); internal struct BITMAPINFOHEADER { public int biSize; public int biWidth; public int biHeight; public ushort biPlanes; public ushort biBitCount; public int biCompression; public int biSizeImage; public int biXPelsPerMeter; public int biYPelsPerMeter; public int biClrUsed; public int biClrImportant; } /// /// BITMAPINFOHEADER members and 3 uints for color table etc. /// internal struct BITMAPINFO { public readonly int biSize; public int biWidth; public int biHeight; public ushort biPlanes; public ushort biBitCount; public int biCompression; public int biSizeImage; public int biXPelsPerMeter; public int biYPelsPerMeter; public int biClrUsed; public int biClrImportant; public fixed uint bmiColors[3]; //info: GetDIBits(DIB_RGB_COLORS) sets 0xFF0000, 0xFF00, 0xFF. Note: with 8-bit colors bitmaps need 256, but this library does not use it. /// /// Sets biSize=sizeof(BITMAPINFOHEADER). Note: it is less than sizeof(BITMAPINFO). /// public BITMAPINFO() { biSize = sizeof(BITMAPINFOHEADER); } /// /// Sets width/height/bitcount/planes fields. Sets biSize=sizeof(BITMAPINFOHEADER). Note: it is less than sizeof(BITMAPINFO). /// public BITMAPINFO(int width, int height, int bitCount = 32) { biSize = sizeof(BITMAPINFOHEADER); biWidth = width; biHeight = height; biBitCount = (ushort)bitCount; biPlanes = 1; } //little tested ///// ///// Gets DIB bits of compatible bitmap. Uses API GetDIBits. Returns null if failed. ///// ///// Bitmap handle. ///// Create top-down DIB. ///// Eg . ///// Use DIB_PAL_COLORS. //public byte[] GetBitmapBits(IntPtr hb, bool topDown, IntPtr dc, bool palColors = false) { // biBitCount = 0; //biBitCount=(ushort)bitCount; //somehow fails if bitCount is 32 and bitmap is 32-bit // if (0 == GetDIBits(dc, hb, 0, 0, null, ref this, palColors ? 1 : 0)) return null; // var r = new byte[biSizeImage]; // fixed (byte* p = r) { // int hei = biHeight; if (topDown) biHeight = -hei; // var k = GetDIBits(dc, hb, 0, hei, p, ref this, palColors ? 1 : 0); // biHeight = hei; // if (k == 0) return null; // } // return r; //} } [DllImport("gdi32.dll")] internal static extern int GetDIBits(IntPtr hdc, IntPtr hbm, int start, int cLines, void* lpvBits, ref BITMAPINFO lpbmi, int usage); /// /// lpbmi can be BITMAPINFOHEADER/BITMAPV5HEADER or BITMAPCOREHEADER. /// [DllImport("gdi32.dll")] internal static extern int SetDIBitsToDevice(IntPtr hdc, int xDest, int yDest, int w, int h, int xSrc, int ySrc, int StartScan, int cLines, void* lpvBits, void* lpbmi, uint ColorUse = 0); //DIB_RGB_COLORS //internal const int WHITE_BRUSH = 0; //internal const int LTGRAY_BRUSH = 1; //internal const int GRAY_BRUSH = 2; //internal const int DKGRAY_BRUSH = 3; //internal const int BLACK_BRUSH = 4; //internal const int NULL_BRUSH = 5; //internal const int HOLLOW_BRUSH = 5; //internal const int WHITE_PEN = 6; //internal const int BLACK_PEN = 7; //internal const int NULL_PEN = 8; //internal const int OEM_FIXED_FONT = 10; //internal const int ANSI_FIXED_FONT = 11; //internal const int ANSI_VAR_FONT = 12; //internal const int SYSTEM_FONT = 13; //internal const int DEVICE_DEFAULT_FONT = 14; //internal const int DEFAULT_PALETTE = 15; //internal const int SYSTEM_FIXED_FONT = 16; //internal const int DEFAULT_GUI_FONT = 17; //internal const int DC_BRUSH = 18; //internal const int DC_PEN = 19; [DllImport("gdi32.dll")] internal static extern IntPtr GetStockObject(int i); [DllImport("gdi32.dll")] internal static extern IntPtr CreateSolidBrush(int color); [DllImport("gdi32.dll")] internal static extern int IntersectClipRect(IntPtr hdc, int left, int top, int right, int bottom); internal struct LOGFONT { public int lfHeight; public int lfWidth; public int lfEscapement; public int lfOrientation; public int lfWeight; public byte lfItalic; public byte lfUnderline; public byte lfStrikeOut; public byte lfCharSet; public byte lfOutPrecision; public byte lfClipPrecision; public byte lfQuality; public byte lfPitchAndFamily; public fixed char lfFaceName[32]; } internal struct NONCLIENTMETRICS { public int cbSize; public int iBorderWidth; public int iScrollWidth; public int iScrollHeight; public int iCaptionWidth; public int iCaptionHeight; public LOGFONT lfCaptionFont; public int iSmCaptionWidth; public int iSmCaptionHeight; public LOGFONT lfSmCaptionFont; public int iMenuWidth; public int iMenuHeight; public LOGFONT lfMenuFont; public LOGFONT lfStatusFont; public LOGFONT lfMessageFont; public int iPaddedBorderWidth; } [DllImport("gdi32.dll")] //does not set last error when fails internal static extern uint GetPixel(IntPtr hdc, int x, int y); #endregion #region advapi32 [DllImport("advapi32.dll")] internal static extern int RegSetValueEx(IntPtr hKey, string lpValueName, int Reserved, Microsoft.Win32.RegistryValueKind dwType, void* lpData, int cbData); [DllImport("advapi32.dll")] internal static extern int RegQueryValueEx(IntPtr hKey, string lpValueName, IntPtr Reserved, out Microsoft.Win32.RegistryValueKind dwType, void* lpData, ref int cbData); internal const uint TOKEN_WRITE = STANDARD_RIGHTS_WRITE | TOKEN_ADJUST_PRIVILEGES | TOKEN_ADJUST_GROUPS | TOKEN_ADJUST_DEFAULT; internal const uint TOKEN_SOURCE_LENGTH = 8; internal const uint TOKEN_READ = STANDARD_RIGHTS_READ | TOKEN_QUERY; internal const uint TOKEN_QUERY_SOURCE = 16; internal const uint TOKEN_QUERY = 8; internal const uint TOKEN_IMPERSONATE = 4; internal const uint TOKEN_EXECUTE = STANDARD_RIGHTS_EXECUTE; internal const uint TOKEN_DUPLICATE = 2; internal const uint TOKEN_AUDIT_SUCCESS_INCLUDE = 1; internal const uint TOKEN_AUDIT_SUCCESS_EXCLUDE = 2; internal const uint TOKEN_AUDIT_FAILURE_INCLUDE = 4; internal const uint TOKEN_AUDIT_FAILURE_EXCLUDE = 8; internal const uint TOKEN_ASSIGN_PRIMARY = 1; internal const uint TOKEN_ALL_ACCESS_P = STANDARD_RIGHTS_REQUIRED | TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_QUERY_SOURCE | TOKEN_ADJUST_PRIVILEGES | TOKEN_ADJUST_GROUPS | TOKEN_ADJUST_DEFAULT; internal const uint TOKEN_ALL_ACCESS = TOKEN_ALL_ACCESS_P | TOKEN_ADJUST_SESSIONID; internal const uint TOKEN_ADJUST_SESSIONID = 256; internal const uint TOKEN_ADJUST_PRIVILEGES = 32; internal const uint TOKEN_ADJUST_GROUPS = 64; internal const uint TOKEN_ADJUST_DEFAULT = 128; [DllImport("advapi32.dll", SetLastError = true)] internal static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out Handle_ TokenHandle); internal enum TOKEN_INFORMATION_CLASS { TokenUser = 1, TokenGroups, TokenPrivileges, TokenOwner, TokenPrimaryGroup, TokenDefaultDacl, TokenSource, TokenType, TokenImpersonationLevel, TokenStatistics, TokenRestrictedSids, TokenSessionId, TokenGroupsAndPrivileges, TokenSessionReference, TokenSandBoxInert, TokenAuditPolicy, TokenOrigin, TokenElevationType, TokenLinkedToken, TokenElevation, TokenHasRestrictions, TokenAccessInformation, TokenVirtualizationAllowed, TokenVirtualizationEnabled, TokenIntegrityLevel, TokenUIAccess, TokenMandatoryPolicy, TokenLogonSid, //Win8 TokenIsAppContainer, TokenCapabilities, TokenAppContainerSid, TokenAppContainerNumber, TokenUserClaimAttributes, TokenDeviceClaimAttributes, TokenRestrictedUserClaimAttributes, TokenRestrictedDeviceClaimAttributes, TokenDeviceGroups, TokenRestrictedDeviceGroups, TokenSecurityAttributes, TokenIsRestricted, TokenProcessTrustLevel, TokenPrivateNameSpace, MaxTokenInfoClass // MaxTokenInfoClass should always be the last enum } [DllImport("advapi32.dll", SetLastError = true)] internal static extern bool GetTokenInformation(HandleRef TokenHandle, TOKEN_INFORMATION_CLASS TokenInformationClass, void* TokenInformation, uint TokenInformationLength, out uint ReturnLength); [DllImport("advapi32.dll")] internal static extern byte* GetSidSubAuthorityCount(IntPtr pSid); [DllImport("advapi32.dll")] internal static extern uint* GetSidSubAuthority(IntPtr pSid, uint nSubAuthority); internal enum SECURITY_IMPERSONATION_LEVEL { SecurityAnonymous, SecurityIdentification, SecurityImpersonation, SecurityDelegation } internal enum TOKEN_TYPE { TokenPrimary = 1, TokenImpersonation } [DllImport("advapi32.dll", SetLastError = true)] internal static extern bool DuplicateTokenEx(IntPtr hExistingToken, uint dwDesiredAccess, SECURITY_ATTRIBUTES lpTokenAttributes, SECURITY_IMPERSONATION_LEVEL ImpersonationLevel, TOKEN_TYPE TokenType, out IntPtr phNewToken); [DllImport("advapi32.dll", SetLastError = true)] internal static extern bool CreateProcessWithTokenW(IntPtr hToken, uint dwLogonFlags, string lpApplicationName, char[] lpCommandLine, uint dwCreationFlags, string lpEnvironment, string lpCurrentDirectory, in STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); internal struct LUID { public uint LowPart; public int HighPart; } [DllImport("advapi32.dll", EntryPoint = "LookupPrivilegeValueW", SetLastError = true)] internal static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, out LUID lpLuid); [StructLayout(LayoutKind.Sequential, Pack = 4)] internal struct LUID_AND_ATTRIBUTES { public LUID Luid; public uint Attributes; } internal struct TOKEN_PRIVILEGES { public int PrivilegeCount; public LUID_AND_ATTRIBUTES Privileges; //[1] } [DllImport("advapi32.dll", SetLastError = true)] internal static extern bool AdjustTokenPrivileges(IntPtr TokenHandle, bool DisableAllPrivileges, in TOKEN_PRIVILEGES NewState, uint BufferLength, [Out] TOKEN_PRIVILEGES[] PreviousState, IntPtr ReturnLength); [StructLayout(LayoutKind.Sequential)] internal sealed class SECURITY_ATTRIBUTES : IDisposable { public int nLength; public void* lpSecurityDescriptor; public int bInheritHandle; /// /// Creates SECURITY_ATTRIBUTES from string security descriptor. /// securityDescriptor can be null; then lpSecurityDescriptor will be null. /// public SECURITY_ATTRIBUTES(string securityDescriptor) { nLength = IntPtr.Size * 3; if (securityDescriptor != null && !ConvertStringSecurityDescriptorToSecurityDescriptor(securityDescriptor, 1, out lpSecurityDescriptor)) throw new AuException(0, "SECURITY_ATTRIBUTES"); } public void Dispose() { if (lpSecurityDescriptor != null) { LocalFree(lpSecurityDescriptor); lpSecurityDescriptor = null; } } ~SECURITY_ATTRIBUTES() => Dispose(); /// /// Creates SECURITY_ATTRIBUTES that allows UAC low IL processes to open the kernel object. /// public static readonly SECURITY_ATTRIBUTES ForLowIL = new SECURITY_ATTRIBUTES("D:NO_ACCESS_CONTROLS:(ML;;NW;;;LW)"); /// /// Creates SECURITY_ATTRIBUTES that allows UAC medium IL processes to open the pipe. /// Like of PipeSecurity that allows ReadWrite for AuthenticatedUserSid. /// public static readonly SECURITY_ATTRIBUTES ForPipes = new SECURITY_ATTRIBUTES("D:(A;;0x12019b;;;AU)"); } [DllImport("advapi32.dll", EntryPoint = "ConvertStringSecurityDescriptorToSecurityDescriptorW", SetLastError = true)] internal static extern bool ConvertStringSecurityDescriptorToSecurityDescriptor(string StringSecurityDescriptor, uint StringSDRevision, out void* SecurityDescriptor, uint* SecurityDescriptorSize = null); //[DllImport("advapi32.dll", EntryPoint = "ConvertSecurityDescriptorToStringSecurityDescriptorW")] //internal static extern bool ConvertSecurityDescriptorToStringSecurityDescriptor(void* SecurityDescriptor, uint RequestedStringSDRevision, uint SecurityInformation, out char* StringSecurityDescriptor, out uint StringSecurityDescriptorLen); [DllImport("advapi32.dll", EntryPoint = "InitiateSystemShutdownW", SetLastError = true)] internal static extern bool InitiateSystemShutdown(string lpMachineName, string lpMessage, int dwTimeout, bool bForceAppsClosed, bool bRebootAfterShutdown); #endregion #region shell32 //[DllImport("shell32.dll")] //internal static extern bool IsUserAnAdmin(); internal const uint SHGFI_ICON = 0x000000100; // get icon; internal const uint SHGFI_DISPLAYNAME = 0x000000200; // get display name; internal const uint SHGFI_TYPENAME = 0x000000400; // get type name; internal const uint SHGFI_ATTRIBUTES = 0x000000800; // get attributes; internal const uint SHGFI_ICONLOCATION = 0x000001000; // get icon location; internal const uint SHGFI_EXETYPE = 0x000002000; // return exe type; internal const uint SHGFI_SYSICONINDEX = 0x000004000; // get system icon index; internal const uint SHGFI_LINKOVERLAY = 0x000008000; // put a link overlay on icon; internal const uint SHGFI_SELECTED = 0x000010000; // show icon in selected state; internal const uint SHGFI_ATTR_SPECIFIED = 0x000020000; // get only specified attributes; internal const uint SHGFI_LARGEICON = 0x000000000; // get large icon; internal const uint SHGFI_SMALLICON = 0x000000001; // get small icon; internal const uint SHGFI_OPENICON = 0x000000002; // get open icon; internal const uint SHGFI_SHELLICONSIZE = 0x000000004; // get shell size icon; internal const uint SHGFI_PIDL = 0x000000008; // pszPath is a pidl; internal const uint SHGFI_USEFILEATTRIBUTES = 0x000000010; // use passed dwFileAttribute; internal const uint SHGFI_ADDOVERLAYS = 0x000000020; // apply the appropriate overlays; internal const uint SHGFI_OVERLAYINDEX = 0x000000040; // Get the index of the overlay; internal struct SHFILEINFO { public IntPtr hIcon; public int iIcon; public uint dwAttributes; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string szDisplayName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)] public string szTypeName; } //[DllImport("shell32.dll", EntryPoint = "SHGetFileInfoW")] //internal static extern nint SHGetFileInfo(string pszPath, uint dwFileAttributes, out SHFILEINFO psfi, int cbFileInfo, uint uFlags); [DllImport("shell32.dll", EntryPoint = "SHGetFileInfoW")] static extern nint _SHGetFileInfo(nint pidl, uint dwFileAttributes, out SHFILEINFO psfi, int cbFileInfo, uint uFlags); internal static nint SHGetFileInfo(nint pidl, out SHFILEINFO psfi, uint uFlags, uint dwFileAttributes = 0) { return _SHGetFileInfo(pidl, dwFileAttributes, out psfi, Api.SizeOf(), uFlags); } internal static nint SHGetFileInfo(string path, out SHFILEINFO psfi, uint uFlags, uint dwFileAttributes = 0) { //if (0 != (uFlags & SHGFI_PIDL)) { //not tested // var pidl = Pidl.FromString_(path); // if (pidl == 0) { psfi = default; return 0; } // try { return _SHGetFileInfo(pidl, dwFileAttributes, out psfi, Api.SizeOf(), uFlags); } // finally { Marshal.FreeCoTaskMem(pidl); } //} else { fixed (char* p = path) return _SHGetFileInfo((nint)p, dwFileAttributes, out psfi, Api.SizeOf(), uFlags); //} } //[DllImport("shell32.dll")] //internal static extern int SHGetDesktopFolder(out IShellFolder ppshf); [DllImport("shell32.dll")] internal static extern int SHParseDisplayName(string pszName, IntPtr pbc, out IntPtr pidl, uint sfgaoIn, uint* psfgaoOut); [DllImport("shell32.dll")] internal static extern int SHGetNameFromIDList(IntPtr pidl, SIGDN sigdnName, out string ppszName); [DllImport("shell32.dll")] internal static extern int SHCreateShellItem(IntPtr pidlParent, IShellFolder psfParent, IntPtr pidl, out IShellItem ppsi); //This classic API supports absolute PIDL and parent+relative PIDL. //There are 2 newer API - SHCreateItemFromIDList (absoulte) and SHCreateItemWithParent (parent+relative). They can get IShellItem2 too, which is currently not useful here. Same speed. //[DllImport("shell32.dll")] //internal static extern int SHCreateItemFromIDList(IntPtr pidl, in Guid riid, out IShellItem ppv); //or IShellItem2 [DllImport("shell32.dll")] internal static extern int SHBindToParent(IntPtr pidl, in Guid riid, out IShellFolder ppv, out IntPtr ppidlLast); [DllImport("shell32.dll")] internal static extern int SHGetPropertyStoreForWindow(wnd hwnd, in Guid riid, out IPropertyStore ppv); internal static PROPERTYKEY PKEY_AppUserModel_ID = new PROPERTYKEY() { fmtid = new Guid(0x9F4C2855, 0x9F79, 0x4B39, 0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3), pid = 5 }; [DllImport("shell32.dll")] internal static extern char** CommandLineToArgvW(string lpCmdLine, out int pNumArgs); [DllImport("shell32.dll", EntryPoint = "Shell_NotifyIconW", SetLastError = true)] internal static extern bool Shell_NotifyIcon(int dwMessage, in NOTIFYICONDATA lpData); internal const int NIM_ADD = 0x0; internal const int NIM_MODIFY = 0x1; internal const int NIM_DELETE = 0x2; internal const int NIM_SETFOCUS = 0x3; internal const int NIM_SETVERSION = 0x4; internal const int NOTIFYICON_VERSION_4 = 4; internal const uint NIF_MESSAGE = 0x1; internal const uint NIF_ICON = 0x2; internal const uint NIF_TIP = 0x4; internal const uint NIF_STATE = 0x8; internal const uint NIF_INFO = 0x10; internal const uint NIF_GUID = 0x20; internal const uint NIF_REALTIME = 0x40; internal const uint NIF_SHOWTIP = 0x80; internal const uint NIS_HIDDEN = 0x1; internal const uint NIS_SHAREDICON = 0x2; internal struct NOTIFYICONDATA { /// /// Sets cbSize, hWnd and uFlags. /// /// /// public NOTIFYICONDATA(wnd wNotify, uint nifFlags = 0) { cbSize = Api.SizeOf(); hWnd = wNotify; uFlags = nifFlags; } public int cbSize; public wnd hWnd; public int uID; public uint uFlags; public int uCallbackMessage; public IntPtr hIcon; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string szTip; public uint dwState; public uint dwStateMask; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string szInfo; public int uVersion; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] public string szInfoTitle; public uint dwInfoFlags; public Guid guidItem; public IntPtr hBalloonIcon; } internal const int NIN_SELECT = 0x400; internal const int NIN_KEYSELECT = 0x401; internal const int NIN_BALLOONSHOW = 0x402; internal const int NIN_BALLOONHIDE = 0x403; internal const int NIN_BALLOONTIMEOUT = 0x404; internal const int NIN_BALLOONUSERCLICK = 0x405; internal const int NIN_POPUPOPEN = 0x406; internal const int NIN_POPUPCLOSE = 0x407; [DllImport("shell32.dll")] internal static extern int Shell_NotifyIconGetRect(in NOTIFYICONIDENTIFIER identifier, out RECT iconLocation); internal struct NOTIFYICONIDENTIFIER { public int cbSize; public wnd hWnd; public int uID; public Guid guidItem; } internal struct SHSTOCKICONINFO { public int cbSize; public IntPtr hIcon; public int iSysImageIndex; public int iIcon; public fixed char szPath[260]; } [DllImport("shell32.dll")] internal static extern int SHGetStockIconInfo(StockIcon siid, uint uFlags, ref SHSTOCKICONINFO psii); [DllImport("shell32.dll", EntryPoint = "#6")] internal static extern int SHDefExtractIcon(string pszIconFile, int iIndex, uint uFlags, IntPtr* phiconLarge, IntPtr* phiconSmall, int nIconSize); internal const int SHIL_LARGE = 0; internal const int SHIL_SMALL = 1; internal const int SHIL_EXTRALARGE = 2; //internal const int SHIL_SYSSMALL = 3; internal const int SHIL_JUMBO = 4; //[DllImport("shell32.dll", EntryPoint = "#727")] //internal static extern int SHGetImageList(int iImageList, in Guid riid, out IImageList ppvObj); [DllImport("shell32.dll", EntryPoint = "#727")] internal static extern int SHGetImageList(int iImageList, in Guid riid, out IntPtr ppvObj); internal const uint SHCNE_RENAMEITEM = 0x1; internal const uint SHCNE_CREATE = 0x2; internal const uint SHCNE_DELETE = 0x4; internal const uint SHCNE_MKDIR = 0x8; internal const uint SHCNE_RMDIR = 0x10; internal const uint SHCNE_MEDIAINSERTED = 0x20; internal const uint SHCNE_MEDIAREMOVED = 0x40; internal const uint SHCNE_DRIVEREMOVED = 0x80; internal const uint SHCNE_DRIVEADD = 0x100; internal const uint SHCNE_NETSHARE = 0x200; internal const uint SHCNE_NETUNSHARE = 0x400; internal const uint SHCNE_ATTRIBUTES = 0x800; internal const uint SHCNE_UPDATEDIR = 0x1000; internal const uint SHCNE_UPDATEITEM = 0x2000; internal const uint SHCNE_SERVERDISCONNECT = 0x4000; internal const uint SHCNE_UPDATEIMAGE = 0x8000; internal const uint SHCNE_DRIVEADDGUI = 0x10000; internal const uint SHCNE_RENAMEFOLDER = 0x20000; internal const uint SHCNE_FREESPACE = 0x40000; internal const uint SHCNE_EXTENDED_EVENT = 0x4000000; internal const uint SHCNE_ASSOCCHANGED = 0x8000000; internal const uint SHCNE_DISKEVENTS = 0x2381F; internal const uint SHCNE_GLOBALEVENTS = 0xC0581E0; internal const uint SHCNE_ALLEVENTS = 0x7FFFFFFF; internal const uint SHCNE_INTERRUPT = 0x80000000; internal const uint SHCNF_IDLIST = 0x0; internal const uint SHCNF_DWORD = 0x3; internal const uint SHCNF_PATH = 0x5; internal const uint SHCNF_PRINTER = 0x6; internal const uint SHCNF_FLUSH = 0x1000; internal const uint SHCNF_FLUSHNOWAIT = 0x3000; internal const uint SHCNF_NOTIFYRECURSIVE = 0x10000; [DllImport("shell32.dll")] internal static extern void SHChangeNotify(uint wEventId, uint uFlags, string dwItem1, string dwItem2); internal const uint SEE_MASK_CONNECTNETDRV = 0x80; internal const uint SEE_MASK_NOZONECHECKS = 0x800000; internal const uint SEE_MASK_UNICODE = 0x4000; internal const uint SEE_MASK_FLAG_NO_UI = 0x400; internal const uint SEE_MASK_INVOKEIDLIST = 0xC; internal const uint SEE_MASK_NOCLOSEPROCESS = 0x40; internal const uint SEE_MASK_NOASYNC = 0x100; internal const uint SEE_MASK_NO_CONSOLE = 0x8000; //internal const uint SEE_MASK_HMONITOR = 0x200000; //internal const uint SEE_MASK_WAITFORINPUTIDLE = 0x2000000; internal const uint SEE_MASK_FLAG_LOG_USAGE = 0x4000000; internal struct SHELLEXECUTEINFO { public int cbSize; public uint fMask; public wnd hwnd; public string lpVerb; public string lpFile; public string lpParameters; public string lpDirectory; public int nShow; public IntPtr hInstApp; public IntPtr lpIDList; public string lpClass; public IntPtr hkeyClass; public uint dwHotKey; public IntPtr hMonitor; public Handle_ hProcess; } [DllImport("shell32.dll", EntryPoint = "ShellExecuteExW", SetLastError = true)] internal static extern bool ShellExecuteEx(ref SHELLEXECUTEINFO pExecInfo); [DllImport("shell32.dll")] internal static extern int SHOpenFolderAndSelectItems(HandleRef pidlFolder, uint cidl, IntPtr[] apidl, uint dwFlags); [DllImport("shell32.dll", EntryPoint = "#152")] internal static extern int ILGetSize(IntPtr pidl); [DllImport("shell32.dll", EntryPoint = "#25")] internal static extern IntPtr ILCombine(IntPtr pidl1, IntPtr pidl2); [DllImport("shell32.dll", EntryPoint = "#21")] internal static extern bool ILIsEqual(IntPtr pidl1, IntPtr pidl2); internal const uint FO_MOVE = 0x1; internal const uint FO_COPY = 0x2; internal const uint FO_DELETE = 0x3; internal const uint FO_RENAME = 0x4; internal const uint FOF_MULTIDESTFILES = 0x1; internal const uint FOF_CONFIRMMOUSE = 0x2; internal const uint FOF_SILENT = 0x4; internal const uint FOF_RENAMEONCOLLISION = 0x8; internal const uint FOF_NOCONFIRMATION = 0x10; internal const uint FOF_WANTMAPPINGHANDLE = 0x20; internal const uint FOF_ALLOWUNDO = 0x40; internal const uint FOF_FILESONLY = 0x80; internal const uint FOF_SIMPLEPROGRESS = 0x100; internal const uint FOF_NOCONFIRMMKDIR = 0x200; internal const uint FOF_NOERRORUI = 0x400; internal const uint FOF_NOCOPYSECURITYATTRIBS = 0x800; internal const uint FOF_NORECURSION = 0x1000; internal const uint FOF_NO_CONNECTED_ELEMENTS = 0x2000; internal const uint FOF_WANTNUKEWARNING = 0x4000; internal const uint FOF_NORECURSEREPARSE = 0x8000; internal const uint FOF_NO_UI = 0x614; internal struct SHFILEOPSTRUCT { public wnd hwnd; public uint wFunc; public string pFrom; public string pTo; public ushort fFlags; //workaround for this problem: the 32-bit version of SHFILEOPSTRUCT uses Pack = 1, ie no 2-byte gap after fFlags. // I don't want to use two versions. Then also would need two versions of code that use this struct. // The last two members are not useful, but we need fAnyOperationsAborted. This workaround gets it through a property function. // Update: we don't need fAnyOperationsAborted. We use FOF_SILENT therefore cannot be aborted. And it is unreliable. // But the workaround is tested, on both platformas. #if use_fAnyOperationsAborted //public bool fAnyOperationsAborted; //BOOL ushort _fAnyOperationsAborted_32, _fAnyOperationsAborted_common, _fAnyOperationsAborted_64; public bool fAnyOperationsAborted { get { if(_fAnyOperationsAborted_common != 0) return true; var v = osVersion.is32BitProcess ? _fAnyOperationsAborted_32 : _fAnyOperationsAborted_64; return v != 0; } } #else private int fAnyOperationsAborted; #endif private IntPtr hNameMappings; private string lpszProgressTitle; //these are private and not used, because would be at invalid offsets on 32-bit } //internal struct SHFILEOPSTRUCT //{ // public wnd hwnd; // public uint wFunc; // public string pFrom; // public string pTo; // public ushort fFlags; // public bool fAnyOperationsAborted; // public IntPtr hNameMappings; // public string lpszProgressTitle; //} //[StructLayout(LayoutKind.Sequential, Pack = 1)] //internal struct SHFILEOPSTRUCT__32 //{ // public wnd hwnd; // public uint wFunc; // public string pFrom; // public string pTo; // public ushort fFlags; // public bool fAnyOperationsAborted; // public IntPtr hNameMappings; // public string lpszProgressTitle; //} [DllImport("shell32.dll", EntryPoint = "SHFileOperationW")] internal static extern int SHFileOperation(in SHFILEOPSTRUCT lpFileOp); [DllImport("shell32.dll", EntryPoint = "DragQueryFileW")] internal static extern int DragQueryFile(IntPtr hDrop, int iFile, char* lpszFile, int cch); [DllImport("shell32.dll")] internal static extern bool IsUserAnAdmin(); [DllImport("shell32.dll", EntryPoint = "SHEmptyRecycleBinW")] internal static extern int SHEmptyRecycleBin(wnd hwnd, string pszRootPath, int dwFlags); #endregion #region shlwapi [DllImport("shlwapi.dll")] static extern int IUnknown_QueryService([MarshalAs(UnmanagedType.IUnknown)] object punk, in Guid guidService, in Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppvOut); public static bool QueryService(object from, in Guid guidService, out T result) where T : class { bool ok = 0 == IUnknown_QueryService(from, guidService, typeof(T).GUID, out var o); result = ok ? (T)o : null; return ok; } //[DllImport("shlwapi.dll")] //internal static extern uint ColorAdjustLuma(uint clrRGB, int n, bool fScale); [DllImport("shlwapi.dll")] internal static extern void ColorRGBToHLS(int clrRGB, out ushort pwHue, out ushort pwLuminance, out ushort pwSaturation); [DllImport("shlwapi.dll")] internal static extern int ColorHLSToRGB(ushort wHue, ushort wLuminance, ushort wSaturation); [DllImport("shlwapi.dll")] internal static extern int PathCreateFromUrlAlloc(string pszIn, out string ppszOut, uint dwFlags = 0); //internal enum ASSOCSTR //{ // ASSOCSTR_COMMAND = 1, // ASSOCSTR_EXECUTABLE, // ASSOCSTR_FRIENDLYDOCNAME, // ASSOCSTR_FRIENDLYAPPNAME, // ASSOCSTR_NOOPEN, // ASSOCSTR_SHELLNEWVALUE, // ASSOCSTR_DDECOMMAND, // ASSOCSTR_DDEIFEXEC, // ASSOCSTR_DDEAPPLICATION, // ASSOCSTR_DDETOPIC, // ASSOCSTR_INFOTIP, // ASSOCSTR_QUICKTIP, // ASSOCSTR_TILEINFO, // ASSOCSTR_CONTENTTYPE, // ASSOCSTR_DEFAULTICON, // ASSOCSTR_SHELLEXTENSION, // ASSOCSTR_DROPTARGET, // ASSOCSTR_DELEGATEEXECUTE, // ASSOCSTR_SUPPORTED_URI_PROTOCOLS, // ASSOCSTR_PROGID, // ASSOCSTR_APPID, // ASSOCSTR_APPPUBLISHER, // ASSOCSTR_APPICONREFERENCE, // ASSOCSTR_MAX //} //[DllImport("shlwapi.dll", EntryPoint = "AssocQueryStringW")] //internal static extern int AssocQueryString(uint flags, /*ASSOCSTR*/ int str, string pszAssoc, string pszExtra, char[] pszOut, ref int pcchOut); ///// ///// Returns executable path of file type. ///// ///// //internal static string AssocQueryString(string dotExt/*, ASSOCSTR what = ASSOCSTR.ASSOCSTR_EXECUTABLE*/) //{ // var b = ApiBuffer_.Char_(300, out var n); // int hr = AssocQueryString(0x20, 2, dotExt, null, b, ref n); //ASSOCF_NOTRUNCATE // if(hr == E_POINTER) hr = AssocQueryString(0x20, 2, dotExt, null, b = ApiBuffer_.Char_(n), ref n); // return hr == 0 ? b.ToString(n) : null; //} #endregion #region comctl32 [DllImport("comctl32.dll")] internal static extern IntPtr ImageList_GetIcon(IntPtr himl, int i, uint flags); [DllImport("comctl32.dll")] internal static extern bool ImageList_GetIconSize(IntPtr himl, out int cx, out int cy); internal const uint TME_LEAVE = 0x2; internal const uint TME_NONCLIENT = 0x10; internal const uint TME_CANCEL = 0x80000000; internal struct TRACKMOUSEEVENT { public int cbSize; public uint dwFlags; public wnd hwndTrack; public int dwHoverTime; public TRACKMOUSEEVENT(wnd w, uint flags, int hoverTime = 0) { cbSize = sizeof(TRACKMOUSEEVENT); hwndTrack = w; dwFlags = flags; dwHoverTime = hoverTime; } } [DllImport("comctl32.dll", EntryPoint = "_TrackMouseEvent")] internal static extern bool TrackMouseEvent(ref TRACKMOUSEEVENT lpEventTrack); /// /// Calls TrackMouseEvent with TME_LEAVE. /// /// /// true to start, false to cancel. internal static bool TrackMouseLeave(wnd w, bool track) { var t = new TRACKMOUSEEVENT(w, track ? TME_LEAVE : TME_LEAVE | TME_CANCEL); return TrackMouseEvent(ref t); } [DllImport("comctl32.dll", EntryPoint = "#380")] internal static extern int LoadIconMetric(IntPtr hinst, nint pszName, int lims, out IntPtr phico); [DllImport("comctl32.dll", EntryPoint = "#410")] internal static extern bool SetWindowSubclass(wnd hWnd, SUBCLASSPROC pfnSubclass, nint uIdSubclass, nint dwRefData = 0); internal delegate nint SUBCLASSPROC(wnd hWnd, int uMsg, nint wParam, nint lParam, nint uIdSubclass, nint dwRefData); [DllImport("comctl32.dll", EntryPoint = "#413")] internal static extern nint DefSubclassProc(wnd hWnd, int uMsg, nint wParam, nint lParam); [DllImport("comctl32.dll", EntryPoint = "#412")] internal static extern bool RemoveWindowSubclass(wnd hWnd, SUBCLASSPROC pfnSubclass, nint uIdSubclass); #endregion #region oleaut32 [DllImport("oleaut32.dll", EntryPoint = "#6")] internal static extern void SysFreeString(char* bstrString); [DllImport("oleaut32.dll", EntryPoint = "#7")] internal static extern int SysStringLen(char* pbstr); [DllImport("oleaut32.dll", EntryPoint = "#4")] internal static extern BSTR SysAllocStringLen(string strIn, int len); [DllImport("oleaut32.dll", EntryPoint = "#2")] internal static extern BSTR SysAllocString(char* psz); [DllImport("oleaut32.dll", EntryPoint = "#147")] internal static extern int VariantChangeTypeEx(ref VARIANT pvargDest, in VARIANT pvarSrc, uint lcid, ushort wFlags, VARENUM vt); [DllImport("oleaut32.dll", EntryPoint = "#9")] internal static extern int VariantClear(ref VARIANT pvarg); [DllImport("oleaut32.dll", EntryPoint = "#35")] internal static extern int GetActiveObject(in Guid rclsid, IntPtr pvReserved, [MarshalAs(UnmanagedType.IUnknown)] out object ppunk); #endregion #region ole32 [DllImport("ole32.dll")] internal static extern int CoCreateInstance(in Guid rclsid, nint pUnkOuter, uint dwClsContext, in Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppv); [DllImport("ole32.dll")] internal static extern int PropVariantClear(ref PROPVARIANT pvar); //internal enum APTTYPE //{ // APTTYPE_CURRENT = -1, // APTTYPE_STA, // APTTYPE_MTA, // APTTYPE_NA, // APTTYPE_MAINSTA //} //[DllImport("ole32.dll")] //internal static extern int CoGetApartmentType(out APTTYPE pAptType, out int pAptQualifier); [DllImport("ole32.dll")] internal static extern int OleInitialize(IntPtr pvReserved); [DllImport("ole32.dll")] internal static extern void OleUninitialize(); [ComImport, Guid("00000122-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IDropTarget { void DragEnter(System.Runtime.InteropServices.ComTypes.IDataObject d, int grfKeyState, POINT pt, ref int effect); void DragOver(int grfKeyState, POINT pt, ref int effect); void DragLeave(); void Drop(System.Runtime.InteropServices.ComTypes.IDataObject d, int grfKeyState, POINT pt, ref int effect); } [DllImport("ole32.dll")] internal static extern int RegisterDragDrop(wnd hwnd, IDropTarget pDropTarget); [DllImport("ole32.dll")] internal static extern int RevokeDragDrop(wnd hwnd); [DllImport("ole32.dll")] internal static extern void ReleaseStgMedium(ref System.Runtime.InteropServices.ComTypes.STGMEDIUM medium); [DllImport("ole32.dll")] internal static extern int CLSIDFromProgID(string lpszProgID, out Guid lpclsid); #endregion #region oleacc [DllImport("oleacc.dll")] internal static extern int AccessibleObjectFromWindow(wnd hwnd, EObjid dwId, in Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object o); [DllImport("oleacc.dll")] internal static extern int AccessibleObjectFromEvent(wnd hwnd, EObjid dwObjectId, int dwChildId, out IntPtr ppacc, out VARIANT pvarChild); [DllImport("oleacc.dll")] internal static extern Handle_ GetProcessHandleFromHwnd(wnd hwnd); [DllImport("oleacc.dll")] internal static extern int CreateStdAccessibleObject(wnd hwnd, EObjid idObject, in Guid riid, out IAccessible ppvObject); [ComImport, Guid("618736e0-3c3d-11cf-810c-00aa00389b71"), InterfaceType(ComInterfaceType.InterfaceIsDual)] internal interface IAccessible { IAccessible get_accParent(); int get_accChildCount(); [PreserveSig] int get_accChild(VarInt varChild, [MarshalAs(UnmanagedType.IDispatch)] out object ppdispChild); string get_accName(VarInt varChild); string get_accValue(VarInt varChild); string get_accDescription(VarInt varChild); VarInt get_accRole(VarInt varChild); VarInt get_accState(VarInt varChild); string get_accHelp(VarInt varChild); int get_accHelpTopic(out string pszHelpFile, VarInt varChild); string get_accKeyboardShortcut(VarInt varChild); object get_accFocus(); object get_accSelection(); string get_accDefaultAction(VarInt varChild); void accSelect(ESelect flagsSelect, VarInt varChild); void accLocation(out int pxLeft, out int pyTop, out int pcxWidth, out int pcyHeight, VarInt varChild); object accNavigate(NAVDIR navDir, VarInt varStart); VarInt accHitTest(int xLeft, int yTop); void accDoDefaultAction(VarInt varChild); void put_accName(VarInt varChild, string szName); void put_accValue(VarInt varChild, string szValue); //NOTE: although some members are obsolete or useless, don't use default implementation, because then exception at run time. } #pragma warning disable 169 internal struct VarInt { ushort _vt, _1, _2, _3; nint _int, _4; public static implicit operator VarInt(int i) => new VarInt { _vt = 3, _int = i + 1 }; public static implicit operator int(VarInt v) { if (v._vt == 3) return (int)v._int - 1; Debug_.Print($"VarInt vt={v._vt}, value={v._int}"); throw new ArgumentException(); } } #pragma warning restore 169 internal enum NAVDIR { UP = 1, DOWN, LEFT, RIGHT, NEXT, PREVIOUS, FIRSTCHILD, LASTCHILD } #endregion #region msvcrt //don't use, because for eg "0xffffffff" returns int.Max instead of -1. //[DllImport("msvcrt.dll", EntryPoint = "wcstol", CallingConvention = CallingConvention.Cdecl)] //internal static extern int strtoi(char* s, char** endPtr = null, int radix = 0); //don't use the u API because they return 1 if the value is too big and the string contains '-'. //[DllImport("msvcrt.dll", EntryPoint = "wcstoul", CallingConvention = CallingConvention.Cdecl)] //internal static extern uint strtoui(char* s, char** endPtr = null, int radix = 0); [DllImport("msvcrt.dll", EntryPoint = "_wcstoi64", CallingConvention = CallingConvention.Cdecl)] internal static extern long strtoi64(char* s, char** endPtr = null, int radix = 0); //info: ntdll also has wcstol, wcstoul, _wcstoui64, but not _wcstoi64. //[DllImport("msvcrt.dll", EntryPoint = "_wcstoui64", CallingConvention = CallingConvention.Cdecl)] //internal static extern ulong strtoui64(char* s, char** endPtr = null, int radix = 0); [DllImport("msvcrt.dll", EntryPoint = "_strtoi64", CallingConvention = CallingConvention.Cdecl)] internal static extern long strtoi64(byte* s, byte** endPtr = null, int radix = 0); //not used //[DllImport("msvcrt.dll", EntryPoint = "_strtoui64", CallingConvention = CallingConvention.Cdecl)] //internal static extern long strtoui64(byte* s, byte** endPtr = null, int radix = 0); //This is used when working with char*. With C# strings use ExtString.ToInt32 etc. internal static int strtoi(char* s, char** endPtr = null, int radix = 0) { return (int)strtoi64(s, endPtr, radix); } //This is used with UTF-8 text. internal static int strtoi(byte* s, byte** endPtr = null, int radix = 0) { return (int)strtoi64(s, endPtr, radix); } #if false //not used, because we have ExtString.ToInt32 etc, which has no overflow problems. But it supports only decimal and hex, not any radix. /// /// Converts part of string to int. /// Returns the int value. /// Returns 0 if the string is null, "" or does not begin with a number; then numberEndIndex will be = startIndex. /// /// String. /// Offset in string where to start parsing. /// Receives offset in string where the number part ends. /// If 0, parses the string as hexadecimal number if starts with "0x", as octal if starts with "0", else as decimal. Else it can be 2 to 36. Examples: 10 - parse as decimal (don't support "0x" etc); 16 - as hexadecimal (eg returns 26 if string is "1A" or "0x1A"); 2 - as binary (eg returns 5 if string is "101"). /// startIndex is invalid. internal static int strtoi(string s, int startIndex, out int numberEndIndex, int radix = 0) { int R = 0, len = s == null ? 0 : s.Length - startIndex; if(len < 0) throw new ArgumentOutOfRangeException("startIndex"); if(len != 0) fixed (char* p = s) { char* t = p + startIndex, e = t; R = strtoi(t, &e, radix); len = (int)(e - t); } numberEndIndex = startIndex + len; return R; } /// /// Converts part of string to long. /// Returns the long value. /// Returns 0 if the string is null, "" or does not begin with a number; then numberEndIndex will be = startIndex. /// /// String. /// Offset in string where to start parsing. /// Receives offset in string where the number part ends. /// If 0, parses the string as hexadecimal number if starts with "0x", as octal if starts with "0", else as decimal. Else it can be 2 to 36. Examples: 10 - parse as decimal (don't support "0x" etc); 16 - as hexadecimal (eg returns 26 if string is "1A" or "0x1A"); 2 - as binary (eg returns 5 if string is "101"). /// startIndex is invalid. internal static long strtoi64(string s, int startIndex, out int numberEndIndex, int radix = 0) { long R = 0; int len = s == null ? 0 : s.Length - startIndex; if(len < 0) throw new ArgumentOutOfRangeException("startIndex"); if(len != 0) fixed (char* p = s) { char* t = p + startIndex, e = t; R = strtoi64(t, &e, radix); len = (int)(e - t); } numberEndIndex = startIndex + len; return R; } internal static int strtoi(string s, int startIndex = 0, int radix = 0) { return strtoi(s, startIndex, out _, radix); } internal static long strtoi64(string s, int startIndex = 0, int radix = 0) { return strtoi64(s, startIndex, out _, radix); } #endif [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern char* _ltoa(int value, byte* s, int radix); [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern void* memmove(void* to, void* from, nint n); //[DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)] //internal static extern void* memset(void* ptr, int ch, nint n); [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern int memcmp(void* p1, void* p2, nint count); #endregion #region winmm [DllImport("winmm.dll")] internal static extern uint timeBeginPeriod(uint uPeriod); [DllImport("winmm.dll")] internal static extern uint timeEndPeriod(uint uPeriod); [DllImport("winmm.dll")] internal static extern uint waveOutSetVolume(IntPtr hwo, uint dwVolume); [DllImport("winmm.dll")] internal static extern uint waveOutGetVolume(IntPtr hwo, out uint pdwVolume); [DllImport("winmm.dll", EntryPoint = "PlaySoundW")] internal static extern bool PlaySound(string pszSound, IntPtr hmod, uint fdwSound); internal const uint SND_FILENAME = 0x20000; internal const uint SND_NODEFAULT = 0x2; internal const uint SND_SYSTEM = 0x200000; internal const uint SND_ASYNC = 0x1; internal const uint SND_ALIAS = 0x10000; internal const uint SND_APPLICATION = 0x80; [DllImport("user32.dll")] internal static extern bool MessageBeep(uint uType); [DllImport("kernel32.dll")] internal static extern bool Beep(int dwFreq, int dwDuration); #endregion #region dwmapi internal enum DWMWA { NCRENDERING_ENABLED = 1, NCRENDERING_POLICY, TRANSITIONS_FORCEDISABLED, ALLOW_NCPAINT, CAPTION_BUTTON_BOUNDS, NONCLIENT_RTL_LAYOUT, FORCE_ICONIC_REPRESENTATION, FLIP3D_POLICY, EXTENDED_FRAME_BOUNDS, HAS_ICONIC_BITMAP, DISALLOW_PEEK, EXCLUDED_FROM_PEEK, CLOAK, CLOAKED, FREEZE_REPRESENTATION, PASSIVE_UPDATE_MODE, } [DllImport("dwmapi.dll")] internal static extern int DwmGetWindowAttribute(wnd hwnd, DWMWA dwAttribute, void* pvAttribute, int cbAttribute); //[DllImport("dwmapi.dll")] //internal static extern int DwmSetWindowAttribute(wnd hwnd, DWMWA dwAttribute, void* pvAttribute, int cbAttribute); //[DllImport("dwmapi.dll")] //internal static extern int DwmRegisterThumbnail(wnd hwndDestination, wnd hwndSource, out IntPtr phThumbnailId); //[DllImport("dwmapi.dll")] //internal static extern int DwmUnregisterThumbnail(IntPtr hThumbnailId); //[DllImport("dwmapi.dll")] //internal static extern int DwmQueryThumbnailSourceSize(IntPtr hThumbnail, out SIZE pSize); //[DllImport("dwmapi.dll")] //internal static extern int DwmUpdateThumbnailProperties(IntPtr hThumbnailId, in DWM_THUMBNAIL_PROPERTIES ptnProperties); //[StructLayout(LayoutKind.Sequential, Pack = 1)] //internal struct DWM_THUMBNAIL_PROPERTIES { // public uint dwFlags; // public RECT rcDestination; // public RECT rcSource; // public byte opacity; // public bool fVisible; // public bool fSourceClientAreaOnly; //} //internal const uint DWM_TNP_SOURCECLIENTAREAONLY = 0x10; //internal const uint DWM_TNP_RECTDESTINATION = 0x1; //internal const uint DWM_TNP_VISIBLE = 0x8; //internal const uint DWM_TNP_RECTSOURCE = 0x2; #endregion #region uxtheme [DllImport("uxtheme.dll")] static extern IntPtr OpenThemeData(wnd hwnd, string pszClassList); [DllImport("uxtheme.dll")] static extern IntPtr OpenThemeDataForDpi(wnd hwnd, string pszClassList, int dpi); internal static IntPtr OpenThemeData(wnd hwnd, string classList, int dpi) { if (osVersion.minWin10_1703) return OpenThemeDataForDpi(hwnd, classList, dpi); return OpenThemeData(hwnd, classList); //never mind: bad on Win8.1 non-primary screen with different DPI than primary if hwnd==0 } [DllImport("uxtheme.dll")] internal static extern int CloseThemeData(IntPtr hTheme); [DllImport("uxtheme.dll")] internal static extern int GetThemePartSize(IntPtr hTheme, IntPtr hdc, int iPartId, int iStateId, RECT* prc, THEMESIZE eSize, out SIZE psz); internal enum THEMESIZE { TS_MIN, TS_TRUE, TS_DRAW } [DllImport("uxtheme.dll")] internal static extern int DrawThemeBackground(IntPtr hTheme, IntPtr hdc, int iPartId, int iStateId, in RECT pRect, RECT* pClipRect = null); //[DllImport("uxtheme.dll")] //internal static extern int SetWindowTheme(wnd hwnd, string pszSubAppName, string pszSubIdList); [DllImport("uxtheme.dll")] internal static extern int GetThemeSysColor(IntPtr hTheme, int iColorId); [DllImport("uxtheme.dll")] internal static extern IntPtr GetThemeSysColorBrush(IntPtr hTheme, int iColorId); [DllImport("uxtheme.dll")] internal static extern int BufferedPaintInit(); //[DllImport("uxtheme.dll")] //internal static extern int BufferedPaintUnInit(); [DllImport("uxtheme.dll")] internal static extern IntPtr BeginBufferedPaint(IntPtr hdcTarget, in RECT prcTarget, BP_BUFFERFORMAT dwFormat, ref BP_PAINTPARAMS pPaintParams, out IntPtr phdc); [DllImport("uxtheme.dll")] internal static extern int EndBufferedPaint(IntPtr hBufferedPaint, bool fUpdateTarget); internal enum BP_BUFFERFORMAT { BPBF_COMPATIBLEBITMAP, BPBF_DIB, BPBF_TOPDOWNDIB, BPBF_TOPDOWNMONODIB } internal struct BP_PAINTPARAMS { public int cbSize; public uint dwFlags; public RECT* prcExclude; //public BLENDFUNCTION* pBlendFunction; uint pBlendFunction; } //internal struct BLENDFUNCTION { // public byte BlendOp; // public byte BlendFlags; // public byte SourceConstantAlpha; // public byte AlphaFormat; //} #endregion #region wtsapi [DllImport("wtsapi32.dll", SetLastError = true)] internal static extern bool WTSTerminateProcess(IntPtr hServer, int ProcessId, int ExitCode); [DllImport("wtsapi32.dll")] internal static extern void WTSFreeMemory(void* pMemory); [DllImport("wtsapi32.dll", EntryPoint = "WTSQuerySessionInformationW", SetLastError = true)] static extern bool _WTSQuerySessionInformation(nint hServer, int SessionId, WTS_INFO_CLASS WTSInfoClass, out char* ppBuffer, out int pBytesReturned); internal enum WTS_INFO_CLASS { WTSInitialProgram, WTSApplicationName, WTSWorkingDirectory, WTSOEMId, WTSSessionId, WTSUserName, WTSWinStationName, WTSDomainName, WTSConnectState, WTSClientBuildNumber, WTSClientName, WTSClientDirectory, WTSClientProductId, WTSClientHardwareId, WTSClientAddress, WTSClientDisplay, WTSClientProtocolType, WTSIdleTime, WTSLogonTime, WTSIncomingBytes, WTSOutgoingBytes, WTSIncomingFrames, WTSOutgoingFrames, WTSClientInfo, WTSSessionInfo, WTSSessionInfoEx, WTSConfigInfo, WTSValidationInfo, WTSSessionAddressV4, WTSIsRemoteSession } internal static unsafe bool WTSQuerySessionInformation(int sessionId, Api.WTS_INFO_CLASS what, out string r) { if (_WTSQuerySessionInformation(0, -1, what, out char* p, out int len)) { r = len > 2 ? new(p, 0, len / 2 - 1) : null; Api.WTSFreeMemory(p); return r != null; } r = null; return false; } internal static unsafe bool WTSQuerySessionInformation(int sessionId, Api.WTS_INFO_CLASS what, out T r) where T : unmanaged { if (_WTSQuerySessionInformation(0, -1, what, out char* p, out int len)) { Debug_.PrintIf(len != sizeof(T), len.ToS()); r = len == sizeof(T) ? *(T*)p : default; Api.WTSFreeMemory(p); return len == sizeof(T); } r = default; return false; } [DllImport("wtsapi32.dll")] internal static extern bool WTSGetChildSessionId(out int pSessionId); #endregion #region ntdll internal struct RTL_OSVERSIONINFOW { public int dwOSVersionInfoSize; public uint dwMajorVersion; public uint dwMinorVersion; public uint dwBuildNumber; public uint dwPlatformId; public fixed char szCSDVersion[128]; } [DllImport("ntdll.dll", ExactSpelling = true)] internal static extern int RtlGetVersion(ref RTL_OSVERSIONINFOW lpVersionInformation); [DllImport("ntdll.dll")] internal static extern uint NtQueryTimerResolution(out uint maxi, out uint mini, out uint current); //info: NtSetTimerResolution can set min 0.5 ms resolution. timeBeginPeriod min 1. [DllImport("ntdll.dll")] internal static extern void MD5Init(out Hash.MD5Context context); [DllImport("ntdll.dll")] internal static extern void MD5Update(ref Hash.MD5Context context, void* data, int dataLen); [DllImport("ntdll.dll")] internal static extern void MD5Final(ref Hash.MD5Context context); #pragma warning disable 169 [DllImport("ntdll.dll")] internal static extern int NtQueryInformationProcess(IntPtr ProcessHandle, int ProcessInformationClass, void* ProcessInformation, int ProcessInformationLength, out int ReturnLength); //all structs are same in 64-bit and 32-bit processes internal record struct PROCESS_BASIC_INFORMATION { long Reserved1; public long PebBaseAddress; long Reserved2_0, Reserved2_1; public long UniqueProcessId; public long ParentProcessId; } internal struct RTL_USER_PROCESS_PARAMETERS { fixed byte Reserved[96]; public UNICODE_STRING ImagePathName; public UNICODE_STRING CommandLine; } internal struct UNICODE_STRING { public ushort Length; public ushort MaximumLength; public char* Buffer; } internal struct SYSTEM_PROCESS_INFORMATION { internal uint NextEntryOffset; internal uint NumberOfThreads; long SpareLi1; long SpareLi2; long SpareLi3; internal long CreateTime; internal long UserTime; internal long KernelTime; internal ushort NameLength; // UNICODE_STRING internal ushort MaximumNameLength; internal IntPtr NamePtr; // This will point into the data block returned by NtQuerySystemInformation internal int BasePriority; internal IntPtr UniqueProcessId; internal IntPtr InheritedFromUniqueProcessId; internal uint HandleCount; internal uint SessionId; //unused members } #pragma warning restore 169 internal const int STATUS_INFO_LENGTH_MISMATCH = unchecked((int)0xC0000004); [DllImport("ntdll.dll")] internal static extern int NtQuerySystemInformation(int five, SYSTEM_PROCESS_INFORMATION* SystemInformation, int SystemInformationLength, out int ReturnLength); [DllImport("ntdll.dll")] internal static extern int NtSuspendProcess(IntPtr handle); [DllImport("ntdll.dll")] internal static extern int NtResumeProcess(IntPtr handle); #endregion #region other [DllImport("msi.dll", EntryPoint = "#217")] internal static extern int MsiGetShortcutTarget(string szShortcutPath, char* szProductCode, char* szFeatureId, char* szComponentCode); [DllImport("msi.dll", EntryPoint = "#173")] internal static extern int MsiGetComponentPath(char* szProduct, char* szComponent, char* lpPathBuf, ref int pcchBuf); [DllImport("powrprof.dll")] internal static extern byte SetSuspendState(byte bHibernate, byte bForce, byte bWakeupEventsDisabled); [DllImport("powrprof.dll")] internal static extern byte IsPwrSuspendAllowed(); //[DllImport("urlmon.dll")] //internal static extern int FindMimeFromData(IntPtr pBC, string pwzUrl, byte[] pBuffer, int cbSize, string pwzMimeProposed, uint dwMimeFlags, out string ppwzMimeOut, uint dwReserved); #endregion #region struct internal struct NEWHEADER { public ushort wReserved; public ushort wResType; public ushort wResCount; }; [StructLayout(LayoutKind.Sequential, Pack = 2)] internal struct ICONDIRENTRY { public byte bWidth; public byte bHeight; public byte bColorCount; public byte bReserved; public ushort wPlanes; public ushort wBitCount; public int dwBytesInRes; public int dwImageOffset; }; #endregion } ================================================ FILE: Au/Api/Api^kernel32.cs ================================================ namespace Au.Types; static unsafe partial class Api { [DllImport("kernel32.dll", SetLastError = true)] //note: without `SetLastError = true` Marshal.GetLastWin32Error is unaware that we set the code to 0 etc and returns old captured error code internal static extern void SetLastError(int errCode); internal const uint FORMAT_MESSAGE_FROM_SYSTEM = 0x1000; internal const uint FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x100; internal const uint FORMAT_MESSAGE_IGNORE_INSERTS = 0x200; [DllImport("kernel32.dll", EntryPoint = "FormatMessageW")] internal static extern int FormatMessage(uint dwFlags, IntPtr lpSource, int code, uint dwLanguageId, char** lpBuffer, int nSize, IntPtr Arguments); [DllImport("kernel32.dll", EntryPoint = "SetDllDirectoryW", SetLastError = true)] internal static extern bool SetDllDirectory(string lpPathName); [SuppressGCTransition] //makes slightly faster. Not faster with [MethodImpl]. [DllImport("kernel32.dll")] internal static extern bool QueryPerformanceCounter(out long lpPerformanceCount); [DllImport("kernel32.dll")] internal static extern bool QueryPerformanceFrequency(out long lpFrequency); [DllImport("kernel32.dll")] internal static extern bool QueryUnbiasedInterruptTime(out long UnbiasedTime); [DllImport("kernel32.dll")] internal static extern long GetTickCount64(); [DllImport("kernel32.dll")] internal static extern int GetTickCount(); internal struct SYSTEMTIME { public ushort wYear; public ushort wMonth; public ushort wDayOfWeek; public ushort wDay; public ushort wHour; public ushort wMinute; public ushort wSecond; public ushort wMilliseconds; } [DllImport("kernel32.dll")] internal static extern void GetLocalTime(out SYSTEMTIME lpSystemTime); [DllImport("kernel32.dll")] internal static extern void GetSystemTimeAsFileTime(out long lpSystemTimeAsFileTime); //[DllImport("kernel32.dll", SetLastError = true)] //internal static extern bool GetThreadTimes(IntPtr hThread, out long lpCreationTime, out long lpExitTime, out long lpKernelTime, out long lpUserTime); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool GetProcessTimes(IntPtr hProcess, out long lpCreationTime, out long lpExitTime, out long lpKernelTime, out long lpUserTime); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool GetThreadTimes(IntPtr hThread, out long lpCreationTime, out long lpExitTime, out long lpKernelTime, out long lpUserTime); [DllImport("kernel32.dll", SetLastError = true)] internal static extern int GetThreadDescription(IntPtr hThread, out char* ppszThreadDescription); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool QueryProcessCycleTime(nint ProcessHandle, out long CycleTime); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool QueryThreadCycleTime(nint ThreadHandle, out long CycleTime); [DllImport("kernel32.dll")] internal static extern bool QueryIdleProcessorCycleTimeEx(ushort Group, ref int BufferLength, long* ProcessorIdleCycleTime); [DllImport("kernel32.dll")] internal static extern ushort GetActiveProcessorGroupCount(); [DllImport("kernel32.dll", SetLastError = true)] internal static extern int GetThreadPriority(nint hThread); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool SetThreadPriority(nint hThread, int nPriority); internal const int THREAD_PRIORITY_TIME_CRITICAL = 15; [DllImport("kernel32.dll", EntryPoint = "CreateEventW", SetLastError = true)] internal static extern IntPtr CreateEvent2(IntPtr lpEventAttributes, bool bManualReset, bool bInitialState, string lpName); internal static Handle_ CreateEvent(bool bManualReset) => new(CreateEvent2(default, bManualReset, false, null)); [DllImport("kernel32.dll", EntryPoint = "OpenEventW", SetLastError = true)] internal static extern IntPtr OpenEvent(uint dwDesiredAccess, bool bInheritHandle, string lpName); internal const uint EVENT_MODIFY_STATE = 0x2; [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool SetEvent(IntPtr hEvent); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool ResetEvent(IntPtr hEvent); [DllImport("kernel32.dll", SetLastError = true)] internal static extern int WaitForSingleObject(IntPtr hHandle, int dwMilliseconds); //[DllImport("kernel32.dll")] //internal static extern int SignalObjectAndWait(IntPtr hObjectToSignal, IntPtr hObjectToWaitOn, int dwMilliseconds, bool bAlertable); //note: don't know why, this often is much slower than setevent/waitforsingleobject. [DllImport("kernel32.dll")] //note: no SetLastError = true internal static extern bool CloseHandle(IntPtr hObject); //currently not used //[DllImport("kernel32.dll")] //note: no SetLastError = true //internal static extern bool CloseHandle(HandleRef hObject); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool SetHandleInformation(IntPtr hObject, uint dwMask, uint dwFlags); [DllImport("kernel32.dll", EntryPoint = "CreateMutexW", SetLastError = true)] internal static extern nint CreateMutex(SECURITY_ATTRIBUTES lpMutexAttributes, bool bInitialOwner, string lpName); [DllImport("kernel32.dll", EntryPoint = "OpenMutexW", SetLastError = true)] internal static extern nint OpenMutex(uint dwDesiredAccess, bool bInheritHandle, string lpName); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool ReleaseMutex(nint hMutex); /// /// Note: use only for private threads. Not everything works like with Thread.Start. For example .NET does not auto-release COM objects when thread ends. /// /// [UnmanagedCallersOnly] [DllImport("kernel32.dll", SetLastError = true)] internal static extern IntPtr CreateThread(nint lpThreadAttributes, nint dwStackSize, delegate* unmanaged lpStartAddress, GCHandle lpParameter, uint dwCreationFlags, out int lpThreadId); [DllImport("kernel32.dll")] internal static extern IntPtr GetCurrentThread(); [SuppressGCTransition] [DllImport("kernel32.dll")] internal static extern int GetCurrentThreadId(); [DllImport("kernel32.dll")] internal static extern IntPtr GetCurrentProcess(); [SuppressGCTransition] [DllImport("kernel32.dll")] internal static extern int GetCurrentProcessId(); [DllImport("kernel32.dll", EntryPoint = "QueryFullProcessImageNameW", SetLastError = true)] internal static extern bool QueryFullProcessImageName(IntPtr hProcess, bool nativeFormat, char* lpExeName, ref int lpdwSize); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool TerminateProcess(IntPtr hProcess, int uExitCode); [DllImport("kernel32.dll")] internal static extern void ExitProcess(int uExitCode); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool IsWow64Process(IntPtr hProcess, out bool Wow64Process); [DllImport("kernel32.dll", SetLastError = true)] internal static extern Handle_ CreateFileMapping(IntPtr hFile, SECURITY_ATTRIBUTES lpFileMappingAttributes, uint flProtect, uint dwMaximumSizeHigh, uint dwMaximumSizeLow, string lpName); [DllImport("kernel32.dll", EntryPoint = "OpenFileMappingW", SetLastError = true)] internal static extern Handle_ OpenFileMapping(uint dwDesiredAccess, bool bInheritHandle, string lpName); [DllImport("kernel32.dll", SetLastError = true)] internal static extern void* MapViewOfFile(IntPtr hFileMappingObject, uint dwDesiredAccess, uint dwFileOffsetHigh, uint dwFileOffsetLow, nint dwNumberOfBytesToMap); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool UnmapViewOfFile(void* lpBaseAddress); [DllImport("kernel32.dll", EntryPoint = "GetModuleHandleW", SetLastError = true)] internal static extern IntPtr GetModuleHandle(string name); //Better use NativeLibrary.TryLoad. //Dlls loaded by LoadLibrary don't find other used dlls from the same directory if it's not the app directory. Need LoadLibraryEx with LOAD_WITH_ALTERED_SEARCH_PATH, and probably NativeLibrary.TryLoad uses it. //[DllImport("kernel32.dll", EntryPoint = "LoadLibraryW", SetLastError = true)] //internal static extern IntPtr LoadLibrary(string lpLibFileName); internal const uint LOAD_LIBRARY_AS_DATAFILE = 0x2; [DllImport("kernel32.dll", EntryPoint = "LoadLibraryExW", SetLastError = true)] internal static extern IntPtr LoadLibraryEx(string lpLibFileName, IntPtr hFile, uint dwFlags); [DllImport("kernel32.dll")] internal static extern bool FreeLibrary(IntPtr hLibModule); [DllImport("kernel32.dll", BestFitMapping = false, SetLastError = true)] internal static extern IntPtr GetProcAddress(IntPtr hModule, [MarshalAs(UnmanagedType.LPStr)] string lpProcName); internal const uint PROCESS_TERMINATE = 0x0001; internal const uint PROCESS_CREATE_THREAD = 0x0002; internal const uint PROCESS_SET_SESSIONID = 0x0004; internal const uint PROCESS_VM_OPERATION = 0x0008; internal const uint PROCESS_VM_READ = 0x0010; internal const uint PROCESS_VM_WRITE = 0x0020; internal const uint PROCESS_DUP_HANDLE = 0x0040; internal const uint PROCESS_CREATE_PROCESS = 0x0080; internal const uint PROCESS_SET_QUOTA = 0x0100; internal const uint PROCESS_SET_INFORMATION = 0x0200; internal const uint PROCESS_QUERY_INFORMATION = 0x0400; internal const uint PROCESS_SUSPEND_RESUME = 0x0800; internal const uint PROCESS_QUERY_LIMITED_INFORMATION = 0x1000; internal const uint PROCESS_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xFFFF; internal const uint DELETE = 0x00010000; internal const uint READ_CONTROL = 0x00020000; internal const uint WRITE_DAC = 0x00040000; internal const uint WRITE_OWNER = 0x00080000; internal const uint SYNCHRONIZE = 0x00100000; internal const uint STANDARD_RIGHTS_REQUIRED = 0x000F0000; internal const uint STANDARD_RIGHTS_READ = READ_CONTROL; internal const uint STANDARD_RIGHTS_WRITE = READ_CONTROL; internal const uint STANDARD_RIGHTS_EXECUTE = READ_CONTROL; internal const uint STANDARD_RIGHTS_ALL = 0x001F0000; internal const uint TIMER_MODIFY_STATE = 0x2; [DllImport("kernel32.dll", SetLastError = true)] internal static extern Handle_ OpenProcess(uint dwDesiredAccess, bool bInheritHandle, int dwProcessId); [DllImport("kernel32.dll", EntryPoint = "GetFullPathNameW", SetLastError = true)] static extern int _GetFullPathName(string lpFileName, int nBufferLength, char* lpBuffer, char** lpFilePart); /// /// Calls API GetFullPathName. /// Returns false if failed or result is same; then r is s. /// r can be same variable as s. /// [SkipLocalsInit] internal static bool GetFullPathName(string s, out string r) { using FastBuffer b = new(); for (; ; ) if (b.GetString(_GetFullPathName(s, b.n, b.p, null), out r, 0, s)) return (object)r != s; } [DllImport("kernel32.dll", EntryPoint = "GetLongPathNameW", SetLastError = true)] static extern int _GetLongPathName(string lpszShortPath, char* lpszLongPath, int cchBuffer); /// /// Calls API GetFullPathName. /// Returns false if failed or result is same; then r is s. /// r can be same variable as s. /// [SkipLocalsInit] internal static bool GetLongPathName(string s, out string r) { using FastBuffer b = new(); for (; ; ) if (b.GetString(_GetLongPathName(s, b.p, b.n), out r, 0, s)) return (object)r != s; } [DllImport("kernel32.dll", EntryPoint = "GetFinalPathNameByHandleW", SetLastError = true)] static extern int _GetFinalPathNameByHandle(IntPtr hFile, char* lpszFilePath, int cchFilePath, uint dwFlags); [SkipLocalsInit] internal static bool GetFinalPathNameByHandle(IntPtr h, out string r, uint dwFlags = 0) { using FastBuffer b = new(); for (; ; ) { g1: if (b.GetString(_GetFinalPathNameByHandle(h, b.p, b.n, dwFlags), out r, 0)) { if (r != null) return r.Length > 0; if (0u != (dwFlags & 3u)) { dwFlags &= ~3u; goto g1; } //if VOLUME_NAME_GUID, fails if network path return false; } } } [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool ProcessIdToSessionId(int dwProcessId, out int pSessionId); internal const uint PAGE_NOACCESS = 0x1; internal const uint PAGE_READONLY = 0x2; internal const uint PAGE_READWRITE = 0x4; internal const uint PAGE_WRITECOPY = 0x8; internal const uint PAGE_EXECUTE = 0x10; internal const uint PAGE_EXECUTE_READ = 0x20; internal const uint PAGE_EXECUTE_READWRITE = 0x40; internal const uint PAGE_EXECUTE_WRITECOPY = 0x80; internal const uint PAGE_GUARD = 0x100; internal const uint PAGE_NOCACHE = 0x200; internal const uint PAGE_WRITECOMBINE = 0x400; internal const uint MEM_COMMIT = 0x1000; internal const uint MEM_RESERVE = 0x2000; internal const uint MEM_DECOMMIT = 0x4000; internal const uint MEM_RELEASE = 0x8000; internal const uint MEM_RESET = 0x80000; internal const uint MEM_TOP_DOWN = 0x100000; internal const uint MEM_WRITE_WATCH = 0x200000; internal const uint MEM_PHYSICAL = 0x400000; internal const uint MEM_RESET_UNDO = 0x1000000; internal const uint MEM_LARGE_PAGES = 0x20000000; [DllImport("kernel32.dll", SetLastError = true)] internal static extern void* VirtualAlloc(void* lpAddress, nint dwSize, uint flAllocationType = MEM_COMMIT | MEM_RESERVE, uint flProtect = PAGE_READWRITE); //note: with PAGE_EXECUTE_READWRITE writing to the memory first time is much slower. [DllImport("kernel32.dll")] internal static extern bool VirtualFree(void* lpAddress, nint dwSize = 0, uint dwFreeType = MEM_RELEASE); [DllImport("kernel32.dll", SetLastError = true)] internal static extern IntPtr VirtualAllocEx(HandleRef hProcess, IntPtr lpAddress, nint dwSize, uint flAllocationType = MEM_COMMIT | MEM_RESERVE, uint flProtect = PAGE_EXECUTE_READWRITE); [DllImport("kernel32.dll")] internal static extern bool VirtualFreeEx(HandleRef hProcess, IntPtr lpAddress, nint dwSize = 0, uint dwFreeType = MEM_RELEASE); [DllImport("kernel32.dll", EntryPoint = "GetFileAttributesW", SetLastError = true)] internal static extern FileAttributes GetFileAttributes(string lpFileName); [DllImport("kernel32.dll", EntryPoint = "SetFileAttributesW", SetLastError = true)] internal static extern bool SetFileAttributes(string lpFileName, FileAttributes dwFileAttributes); [StructLayout(LayoutKind.Sequential, Pack = 4)] internal struct WIN32_FILE_ATTRIBUTE_DATA { public FileAttributes dwFileAttributes; public FILETIME ftCreationTime; public FILETIME ftLastAccessTime; public FILETIME ftLastWriteTime; public uint nFileSizeHigh; public uint nFileSizeLow; public long Size => (long)nFileSizeHigh << 32 | nFileSizeLow; } [DllImport("kernel32.dll", EntryPoint = "GetFileAttributesExW", SetLastError = true)] internal static extern bool GetFileAttributesEx(string lpFileName, int zero, out WIN32_FILE_ATTRIBUTE_DATA lpFileInformation); [DllImport("kernel32.dll", EntryPoint = "SearchPathW", SetLastError = true)] static extern int _SearchPath(string lpPath, string lpFileName, string lpExtension, int nBufferLength, char* lpBuffer, char** lpFilePart); /// /// Calls API SearchPath. Returns full path, or null if not found. /// /// Parent directory or null. /// /// null or extension like ".ext" to add if lpFileName is without extension. [SkipLocalsInit] internal static string SearchPath(string lpPath, string lpFileName, string lpExtension = null) { using FastBuffer b = new(); for (; ; ) if (b.GetString(_SearchPath(lpPath, lpFileName, lpExtension, b.n, b.p, null), out var s)) return s; } internal const uint BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODE = 0x1; internal const uint BASE_SEARCH_PATH_DISABLE_SAFE_SEARCHMODE = 0x10000; internal const uint BASE_SEARCH_PATH_PERMANENT = 0x8000; [DllImport("kernel32.dll")] internal static extern bool SetSearchPathMode(uint Flags); internal const uint SEM_FAILCRITICALERRORS = 0x1; internal const uint SEM_NOGPFAULTERRORBOX = 0x2; [DllImport("kernel32.dll")] internal static extern uint SetErrorMode(uint uMode); //[DllImport("kernel32.dll")] //internal static extern uint GetErrorMode(); //[DllImport("kernel32.dll", SetLastError = true)] //internal static extern IntPtr LocalAlloc(uint uFlags, nint uBytes); [DllImport("kernel32.dll")] internal static extern IntPtr LocalFree(void* hMem); [DllImport("kernel32.dll", EntryPoint = "lstrcpynW")] internal static extern char* lstrcpyn(char* sTo, string sFrom, int sToBufferLength); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool Wow64DisableWow64FsRedirection(out IntPtr OldValue); [DllImport("kernel32.dll")] internal static extern bool Wow64RevertWow64FsRedirection(IntPtr OlValue); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool GetExitCodeProcess(IntPtr hProcess, out int lpExitCode); [DllImport("kernel32.dll")] internal static extern IntPtr GetProcessHeap(); [DllImport("kernel32.dll")] internal static extern void* HeapAlloc(IntPtr hHeap, uint dwFlags, nint dwBytes); [DllImport("kernel32.dll")] internal static extern void* HeapReAlloc(IntPtr hHeap, uint dwFlags, void* lpMem, nint dwBytes); [DllImport("kernel32.dll")] internal static extern bool HeapFree(IntPtr hHeap, uint dwFlags, void* lpMem); internal const int CP_UTF8 = 65001; internal const uint MB_ERR_INVALID_CHARS = 0x8; internal const uint WC_ERR_INVALID_CHARS = 0x80; [DllImport("kernel32.dll")] internal static extern int MultiByteToWideChar(uint CodePage, uint dwFlags, byte* lpMultiByteStr, int cbMultiByte, char* lpWideCharStr, int cchWideChar); [DllImport("kernel32.dll")] internal static extern int WideCharToMultiByte(uint CodePage, uint dwFlags, char* lpWideCharStr, int cchWideChar, byte* lpMultiByteStr, int cbMultiByte, IntPtr lpDefaultChar = default, int* lpUsedDefaultChar = null); [Flags] internal enum Access : uint { } internal const Access FILE_READ_DATA = (Access)0x1; internal const Access FILE_LIST_DIRECTORY = (Access)0x1; internal const Access FILE_WRITE_DATA = (Access)0x2; internal const Access FILE_ADD_FILE = (Access)0x2; internal const Access FILE_APPEND_DATA = (Access)0x4; internal const Access FILE_ADD_SUBDIRECTORY = (Access)0x4; internal const Access FILE_CREATE_PIPE_INSTANCE = (Access)0x4; internal const Access FILE_READ_EA = (Access)0x8; internal const Access FILE_WRITE_EA = (Access)0x10; internal const Access FILE_EXECUTE = (Access)0x20; internal const Access FILE_TRAVERSE = (Access)0x20; internal const Access FILE_DELETE_CHILD = (Access)0x40; internal const Access FILE_READ_ATTRIBUTES = (Access)0x80; internal const Access FILE_WRITE_ATTRIBUTES = (Access)0x100; internal const Access FILE_ALL_ACCESS = (Access)0x1F01FF; internal const Access FILE_GENERIC_READ = (Access)0x120089; internal const Access FILE_GENERIC_WRITE = (Access)0x120116; internal const Access FILE_GENERIC_EXECUTE = (Access)0x1200A0; internal const Access GENERIC_READ = (Access)0x80000000; internal const Access GENERIC_WRITE = (Access)0x40000000; internal enum CfCreation { } internal const CfCreation CREATE_NEW = (CfCreation)1; internal const CfCreation CREATE_ALWAYS = (CfCreation)2; internal const CfCreation OPEN_EXISTING = (CfCreation)3; internal const CfCreation OPEN_ALWAYS = (CfCreation)4; internal const CfCreation TRUNCATE_EXISTING = (CfCreation)5; [Flags] internal enum CfShare : uint { } internal const CfShare FILE_SHARE_READ = (CfShare)0x1; internal const CfShare FILE_SHARE_WRITE = (CfShare)0x2; internal const CfShare FILE_SHARE_DELETE = (CfShare)0x4; internal const CfShare FILE_SHARE_ALL = (CfShare)0x7; //the commented out attributes are not documented for CreateFile internal const uint FILE_ATTRIBUTE_READONLY = 0x1; internal const uint FILE_ATTRIBUTE_HIDDEN = 0x2; internal const uint FILE_ATTRIBUTE_SYSTEM = 0x4; //internal const uint FILE_ATTRIBUTE_DIRECTORY = 0x10; internal const uint FILE_ATTRIBUTE_ARCHIVE = 0x20; //internal const uint FILE_ATTRIBUTE_DEVICE = 0x40; internal const uint FILE_ATTRIBUTE_NORMAL = 0x80; internal const uint FILE_ATTRIBUTE_TEMPORARY = 0x100; //internal const uint FILE_ATTRIBUTE_SPARSE_FILE = 0x200; //internal const uint FILE_ATTRIBUTE_REPARSE_POINT = 0x400; //internal const uint FILE_ATTRIBUTE_COMPRESSED = 0x800; internal const uint FILE_ATTRIBUTE_OFFLINE = 0x1000; //internal const uint FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x2000; internal const uint FILE_ATTRIBUTE_ENCRYPTED = 0x4000; //internal const uint FILE_ATTRIBUTE_INTEGRITY_STREAM = 0x8000; //internal const uint FILE_ATTRIBUTE_VIRTUAL = 0x10000; //internal const uint FILE_ATTRIBUTE_NO_SCRUB_DATA = 0x20000; internal const uint FILE_FLAG_WRITE_THROUGH = 0x80000000; internal const uint FILE_FLAG_OVERLAPPED = 0x40000000; internal const uint FILE_FLAG_NO_BUFFERING = 0x20000000; internal const uint FILE_FLAG_RANDOM_ACCESS = 0x10000000; internal const uint FILE_FLAG_SEQUENTIAL_SCAN = 0x8000000; internal const uint FILE_FLAG_DELETE_ON_CLOSE = 0x4000000; internal const uint FILE_FLAG_BACKUP_SEMANTICS = 0x2000000; internal const uint FILE_FLAG_POSIX_SEMANTICS = 0x1000000; internal const uint FILE_FLAG_SESSION_AWARE = 0x800000; internal const uint FILE_FLAG_OPEN_REPARSE_POINT = 0x200000; internal const uint FILE_FLAG_OPEN_NO_RECALL = 0x100000; internal const uint FILE_FLAG_FIRST_PIPE_INSTANCE = 0x80000; internal const uint FILE_FLAG_OPEN_REQUIRING_OPLOCK = 0x40000; [DllImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true)] static extern IntPtr _CreateFile(string lpFileName, Access dwDesiredAccess, CfShare dwShareMode, SECURITY_ATTRIBUTES lpSecurityAttributes, CfCreation creationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile); internal static Handle_ CreateFile(string lpFileName, Access dwDesiredAccess, CfShare dwShareMode, CfCreation creationDisposition, uint dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL, IntPtr hTemplateFile = default, SECURITY_ATTRIBUTES lpSecurityAttributes = null) => new Handle_(_CreateFile(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, creationDisposition, dwFlagsAndAttributes, hTemplateFile)); //note: cannot return Handle_ directly from API because returns -1 if failed. The ctor then makes 0. //note: not using parameter types SECURITY_ATTRIBUTES and OVERLAPPED* because it makes JIT-compiling much slower in some time-critical places. [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool ReadFile(IntPtr hFile, void* lpBuffer, int nBytesToRead, out int nBytesRead, void* lpOverlapped = null); internal static bool ReadFileArr(IntPtr hFile, byte[] a, out int nBytesRead, void* lpOverlapped = null) { fixed (byte* p = a) return ReadFile(hFile, p, a.Length, out nBytesRead, lpOverlapped); } internal static bool ReadFileArr(IntPtr hFile, out byte[] a, int size, out int nBytesRead, void* lpOverlapped = null) { a = new byte[size]; return ReadFileArr(hFile, a, out nBytesRead, lpOverlapped); } //internal static byte[] ReadFileArr(string file) { // using var h = CreateFile(file, Api.GENERIC_READ, FILE_SHARE_ALL, OPEN_EXISTING); // if (h.Is0 || !GetFileSizeEx(h, out long size) || !ReadFileArr(h, out var a, (int)size, out _)) throw new AuException(0); // return a; //} [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool WriteFile(IntPtr hFile, void* lpBuffer, int nBytesToWrite, out int nBytesWritten, void* lpOverlapped = null); //note: lpNumberOfBytesWritten can be null only if lpOverlapped is not null. //note: don't use overloads, because we Jit_.Compile("WriteFile"). internal static bool WriteFile2(IntPtr hFile, RByte a, out int nBytesWritten) { fixed (byte* p = a) return WriteFile(hFile, p, a.Length, out nBytesWritten); } //internal static bool WriteFile2(IntPtr hFile, RByte a, out int nBytesWritten, void* lpOverlapped) //{ // fixed (byte* p = a) return WriteFile(hFile, p, a.Length, out nBytesWritten, lpOverlapped); //} internal struct OVERLAPPED { nint _1, _2; int _3, _4; public IntPtr hEvent; } [StructLayout(LayoutKind.Sequential, Pack = 4)] internal struct BY_HANDLE_FILE_INFORMATION { public uint dwFileAttributes; public long ftCreationTime; public long ftLastAccessTime; public long ftLastWriteTime; public uint dwVolumeSerialNumber; uint _nFileSizeHigh; uint _nFileSizeLow; public uint nNumberOfLinks; uint _nFileIndexHigh; uint _nFileIndexLow; public long FileSize => (long)((ulong)_nFileSizeHigh << 32 | _nFileSizeLow); public long FileIndex => (long)((ulong)_nFileIndexHigh << 32 | _nFileIndexLow); } [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool GetFileInformationByHandle(IntPtr hFile, out BY_HANDLE_FILE_INFORMATION lpFileInformation); //internal enum FILE_INFO_BY_HANDLE_CLASS //{ // FileBasicInfo, // FileStandardInfo, // FileNameInfo, // FileRenameInfo, // FileDispositionInfo, // FileAllocationInfo, // FileEndOfFileInfo, // FileStreamInfo, // FileCompressionInfo, // FileAttributeTagInfo, // FileIdBothDirectoryInfo, // FileIdBothDirectoryRestartInfo, // FileIoPriorityHintInfo, // FileRemoteProtocolInfo, // FileFullDirectoryInfo, // FileFullDirectoryRestartInfo, // FileStorageInfo, // FileAlignmentInfo, // FileIdInfo, // FileIdExtdDirectoryInfo, // FileIdExtdDirectoryRestartInfo, // MaximumFileInfoByHandleClass //} //internal struct FILE_BASIC_INFO //{ // public long CreationTime; // public long LastAccessTime; // public long LastWriteTime; // public long ChangeTime; // public uint FileAttributes; //} //[DllImport("kernel32.dll", SetLastError = true)] //internal static extern bool GetFileInformationByHandleEx(IntPtr hFile, int FileInformationClass, void* lpFileInformation, int dwBufferSize); //[DllImport("kernel32.dll", SetLastError = true)] //internal static extern bool SetFileInformationByHandle(IntPtr hFile, int FileInformationClass, void* lpFileInformation, int dwBufferSize); internal const int FILE_BEGIN = 0; internal const int FILE_CURRENT = 1; internal const int FILE_END = 2; [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool SetFilePointerEx(IntPtr hFile, long liDistanceToMove, long* lpNewFilePointer, int dwMoveMethod); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool SetEndOfFile(IntPtr hFile); [DllImport("kernel32.dll", EntryPoint = "CreateMailslotW", SetLastError = true)] static extern IntPtr _CreateMailslot(string lpName, uint nMaxMessageSize, int lReadTimeout, SECURITY_ATTRIBUTES lpSecurityAttributes); internal static Handle_ CreateMailslot(string lpName, uint nMaxMessageSize, int lReadTimeout, SECURITY_ATTRIBUTES lpSecurityAttributes) => new Handle_(_CreateMailslot(lpName, nMaxMessageSize, lReadTimeout, lpSecurityAttributes)); //note: cannot return Handle_ directly from API because returns -1 if failed. The ctor then makes 0. [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool GetMailslotInfo(IntPtr hMailslot, uint* lpMaxMessageSize, out int lpNextSize, out int lpMessageCount, int* lpReadTimeout = null); [DllImport("kernel32.dll")] internal static extern int GetApplicationUserModelId(IntPtr hProcess, ref int AppModelIDLength, char* sbAppUserModelID); [DllImport("kernel32.dll", EntryPoint = "GetEnvironmentVariableW", SetLastError = true)] static extern int _GetEnvironmentVariable(string lpName, char* lpBuffer, int nSize); /// /// Calls API GetEnvironmentVariable. /// Returns null if variable not found. /// Does not support folders.X. /// /// Case-insensitive name. Without %. [SkipLocalsInit] internal static string GetEnvironmentVariable(string name) { using FastBuffer b = new(); for (; ; ) if (b.GetString(_GetEnvironmentVariable(name, b.p, b.n), out var s)) return s; } /// /// Returns true if environment variable exists. /// internal static bool EnvironmentVariableExists(string name) => 0 != _GetEnvironmentVariable(name, null, 0); [DllImport("kernel32.dll", EntryPoint = "SetEnvironmentVariableW", SetLastError = true)] internal static extern bool SetEnvironmentVariable(string lpName, string lpValue); [DllImport("kernel32.dll", EntryPoint = "ExpandEnvironmentStringsW")] static extern int _ExpandEnvironmentStrings(string lpSrc, char* lpDst, int nSize); /// /// Calls API ExpandEnvironmentStrings. /// Returns false if failed or result is same; then r is s. /// r can be same variable as s. /// [SkipLocalsInit] internal static bool ExpandEnvironmentStrings(string s, out string r) { using FastBuffer b = new(); for (; ; ) if (b.GetString(_ExpandEnvironmentStrings(s, b.p, b.n), out r, BSFlags.ReturnsLengthWith0, s)) return (object)r != s; } [DllImport("kernel32.dll", EntryPoint = "GetEnvironmentStringsW")] internal static extern char* GetEnvironmentStrings(); [DllImport("kernel32.dll", EntryPoint = "FreeEnvironmentStringsW")] internal static extern bool FreeEnvironmentStrings(char* penv); [DllImport("kernel32.dll", SetLastError = true)] internal static extern int GetProcessId(IntPtr Process); internal struct FILETIME { public uint dwLowDateTime; public uint dwHighDateTime; public static implicit operator long(FILETIME ft) => (long)((ulong)ft.dwHighDateTime << 32 | ft.dwLowDateTime); public static implicit operator FILETIME(long ft) => new() { dwHighDateTime = (uint)(ft >>> 32), dwLowDateTime = (uint)ft }; } internal struct WIN32_FIND_DATA { public FileAttributes dwFileAttributes; public FILETIME ftCreationTime; public FILETIME ftLastAccessTime; public FILETIME ftLastWriteTime; public uint nFileSizeHigh; public uint nFileSizeLow; public uint dwReserved0; public uint dwReserved1; public fixed char cFileName[260]; public fixed char cAlternateFileName[14]; /// /// Returns cFileName as string, or null if it's ".." or ".". /// public unsafe string Name { get { fixed (char* p = cFileName) { if (p[0] == '.') { if (p[1] == '\0') return null; if (p[1] == '.' && p[2] == '\0') return null; } return new string(p); } } } /// /// Returns nonzero if this is a NTFS link: 1 symlink, 2 mount, 3 other. /// public int IsNtfsLink => dwFileAttributes.Has(FileAttributes.ReparsePoint) && 0 != (dwReserved0 & 0x20000000) ? dwReserved0 switch { 0xA000000C => 1, 0xA0000003 => 2, _ => 3 } : 0; } [DllImport("kernel32.dll", EntryPoint = "FindFirstFileW", SetLastError = true)] internal static extern IntPtr FindFirstFile(string lpFileName, out WIN32_FIND_DATA lpFindFileData); [DllImport("kernel32.dll", EntryPoint = "FindNextFileW", SetLastError = true)] internal static extern bool FindNextFile(IntPtr hFindFile, out WIN32_FIND_DATA lpFindFileData); [DllImport("kernel32.dll")] internal static extern bool FindClose(IntPtr hFindFile); #if TEST_FINDFIRSTFILEEX internal enum FINDEX_INFO_LEVELS { FindExInfoStandard, FindExInfoBasic, FindExInfoMaxInfoLevel } internal const uint FIND_FIRST_EX_LARGE_FETCH = 0x2; [DllImport("kernel32.dll", EntryPoint = "FindFirstFileExW")] internal static extern IntPtr FindFirstFileEx(string lpFileName, FINDEX_INFO_LEVELS fInfoLevelId, out WIN32_FIND_DATA lpFindFileData, int fSearchOp, IntPtr lpSearchFilter, uint dwAdditionalFlags); #endif internal const uint MOVEFILE_REPLACE_EXISTING = 0x1; internal const uint MOVEFILE_COPY_ALLOWED = 0x2; internal const uint MOVEFILE_DELAY_UNTIL_REBOOT = 0x4; internal const uint MOVEFILE_WRITE_THROUGH = 0x8; internal const uint MOVEFILE_CREATE_HARDLINK = 0x10; internal const uint MOVEFILE_FAIL_IF_NOT_TRACKABLE = 0x20; [DllImport("kernel32.dll", EntryPoint = "MoveFileExW", SetLastError = true)] internal static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, uint dwFlags); //[DllImport("kernel32.dll", EntryPoint = "CopyFileW", SetLastError = true)] //internal static extern bool CopyFile(string lpExistingFileName, string lpNewFileName, bool bFailIfExists); internal const uint COPY_FILE_FAIL_IF_EXISTS = 0x1; internal const uint COPY_FILE_RESTARTABLE = 0x2; internal const uint COPY_FILE_OPEN_SOURCE_FOR_WRITE = 0x4; internal const uint COPY_FILE_ALLOW_DECRYPTED_DESTINATION = 0x8; internal const uint COPY_FILE_COPY_SYMLINK = 0x800; internal const uint COPY_FILE_NO_BUFFERING = 0x1000; [DllImport("kernel32.dll", EntryPoint = "CopyFileExW", SetLastError = true)] static extern bool CopyFileEx(string lpExistingFileName, string lpNewFileName, nint lpProgressRoutine, IntPtr lpData, int* pbCancel, uint dwCopyFlags); internal static bool CopyFileEx(string lpExistingFileName, string lpNewFileName, uint dwCopyFlags) { if (!CopyFileEx(lpExistingFileName, lpNewFileName, 0, 0, null, dwCopyFlags)) return false; //Workaround for: when copying in Vmware virtual PC from host path like @"\\vmware-host\Shared Folders\...", adds Readonly attribute. // Also GetFileAttributes[Ex] for the source adds Readonly. But FindFirstFile[Ex] doesn't. if (GetFileAttributes(lpNewFileName) is var a1 && a1.Has(FileAttributes.ReadOnly) && a1 != (FileAttributes)(-1)) { var h = FindFirstFile(lpExistingFileName, out var d); if (h != -1) { FindClose(h); if (!d.dwFileAttributes.Has(FileAttributes.ReadOnly)) { Debug_.Print("Readonly attribute"); SetFileAttributes(lpNewFileName, d.dwFileAttributes); } } } return true; } [DllImport("kernel32.dll", EntryPoint = "DeleteFileW", SetLastError = true)] internal static extern bool DeleteFile(string lpFileName); [DllImport("kernel32.dll", EntryPoint = "RemoveDirectoryW", SetLastError = true)] internal static extern bool RemoveDirectory(string lpPathName); [DllImport("kernel32.dll", EntryPoint = "CreateDirectoryW", SetLastError = true)] internal static extern bool CreateDirectory(string lpPathName, IntPtr lpSecurityAttributes); //ref SECURITY_ATTRIBUTES [DllImport("kernel32.dll", EntryPoint = "CreateDirectoryExW", SetLastError = true)] internal static extern bool CreateDirectoryEx(string lpTemplateDirectory, string lpNewDirectory, IntPtr lpSecurityAttributes); //ref SECURITY_ATTRIBUTES [DllImport("kernel32.dll", EntryPoint = "ReplaceFileW", SetLastError = true)] internal static extern bool ReplaceFile(string lpReplacedFileName, string lpReplacementFileName, string lpBackupFileName, uint dwReplaceFlags, IntPtr lpExclude = default, IntPtr lpReserved = default); [DllImport("kernel32.dll", EntryPoint = "GlobalAddAtomW")] internal static extern ushort GlobalAddAtom(string lpString); [DllImport("kernel32.dll")] internal static extern ushort GlobalDeleteAtom(ushort nAtom); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool ReadProcessMemory(HandleRef hProcess, IntPtr lpBaseAddress, void* lpBuffer, nint nSize, nint* lpNumberOfBytesRead); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool WriteProcessMemory(HandleRef hProcess, IntPtr lpBaseAddress, void* lpBuffer, nint nSize, nint* lpNumberOfBytesWritten); [DllImport("kernel32.dll", SetLastError = true)] internal extern static IntPtr CreateActCtx(in ACTCTX actctx); [DllImport("kernel32.dll", SetLastError = true)] internal extern static bool ActivateActCtx(IntPtr hActCtx, out IntPtr lpCookie); [DllImport("kernel32.dll", SetLastError = true)] internal extern static bool DeactivateActCtx(int dwFlags, IntPtr lpCookie); [DllImport("kernel32.dll")] internal static extern void ReleaseActCtx(IntPtr hActCtx); internal const int ACTCTX_FLAG_RESOURCE_NAME_VALID = 0x8; internal const int ACTCTX_FLAG_HMODULE_VALID = 0x80; internal struct ACTCTX { public int cbSize; public uint dwFlags; public string lpSource; public ushort wProcessorArchitecture; public ushort wLangId; public IntPtr lpAssemblyDirectory; public IntPtr lpResourceName; public IntPtr lpApplicationName; public IntPtr hModule; } //internal const uint THREAD_TERMINATE = 0x1; internal const uint THREAD_SUSPEND_RESUME = 0x2; internal const uint THREAD_SET_CONTEXT = 0x10; internal const uint THREAD_QUERY_LIMITED_INFORMATION = 0x800; internal const uint THREAD_ALL_ACCESS = 0x1FFFFF; [DllImport("kernel32.dll", SetLastError = true)] internal static extern Handle_ OpenThread(uint dwDesiredAccess, bool bInheritHandle, int dwThreadId); [DllImport("kernel32.dll", SetLastError = true)] internal static extern int SuspendThread(IntPtr hThread); [DllImport("kernel32.dll", SetLastError = true)] internal static extern uint ResumeThread(IntPtr hThread); //[DllImport("kernel32.dll", SetLastError = true)] //internal static extern bool TerminateThread(IntPtr hThread, int dwExitCode); internal const uint GMEM_FIXED = 0x0; internal const uint GMEM_MOVEABLE = 0x2; internal const uint GMEM_ZEROINIT = 0x40; [DllImport("kernel32.dll", SetLastError = true)] internal static extern IntPtr GlobalAlloc(uint uFlags, nint dwBytes); [DllImport("kernel32.dll", SetLastError = true)] internal static extern IntPtr GlobalFree(IntPtr hMem); [DllImport("kernel32.dll", SetLastError = true)] internal static extern IntPtr GlobalLock(IntPtr hMem); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool GlobalUnlock(IntPtr hMem); [DllImport("kernel32.dll", SetLastError = true)] internal static extern nint GlobalSize(IntPtr hMem); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool GetFileSizeEx(IntPtr hFile, out long lpFileSize); [DllImport("kernel32.dll", SetLastError = true)] internal static extern int WaitForMultipleObjectsEx(int nCount, IntPtr* pHandles, bool bWaitAll, int dwMilliseconds, bool bAlertable); [DllImport("kernel32.dll")] internal static extern int SleepEx(int dwMilliseconds, bool bAlertable); [DllImport("kernel32.dll", EntryPoint = "GetStartupInfoW")] internal static extern void GetStartupInfo(out STARTUPINFO lpStartupInfo); internal struct STARTUPINFO { public int cb; public IntPtr lpReserved; public char* lpDesktop; public char* lpTitle; public int dwX; public int dwY; public int dwXSize; public int dwYSize; public int dwXCountChars; public int dwYCountChars; public uint dwFillAttribute; public uint dwFlags; public ushort wShowWindow; public ushort cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; } internal struct STARTUPINFOEX { public STARTUPINFO StartupInfo; public IntPtr lpAttributeList; } internal struct PROCESS_INFORMATION : IDisposable { public Handle_ hProcess; public Handle_ hThread; public int dwProcessId; public int dwThreadId; public void Dispose() { hThread.Dispose(); hProcess.Dispose(); } } //CreateProcess flags internal const uint CREATE_SUSPENDED = 0x4; internal const uint CREATE_NEW_CONSOLE = 0x10; internal const uint CREATE_UNICODE_ENVIRONMENT = 0x400; internal const uint EXTENDED_STARTUPINFO_PRESENT = 0x80000; //STARTUPINFO flags internal const uint STARTF_USESHOWWINDOW = 0x1; internal const uint STARTF_FORCEOFFFEEDBACK = 0x80; internal const uint STARTF_USESTDHANDLES = 0x100; [DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true)] internal static extern bool CreateProcess(string lpApplicationName, char[] lpCommandLine, SECURITY_ATTRIBUTES lpProcessAttributes, SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, string lpEnvironment, string lpCurrentDirectory, in STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); [DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUserW", SetLastError = true)] internal static extern bool CreateProcessAsUser(IntPtr hToken, string lpApplicationName, char[] lpCommandLine, SECURITY_ATTRIBUTES lpProcessAttributes, SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, string lpEnvironment, string lpCurrentDirectory, in STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); [DllImport("kernel32.dll", EntryPoint = "CreateWaitableTimerW", SetLastError = true)] internal static extern Handle_ CreateWaitableTimer(SECURITY_ATTRIBUTES lpTimerAttributes, bool bManualReset, string lpTimerName); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool SetWaitableTimer(IntPtr hTimer, ref long lpDueTime, int lPeriod = 0, IntPtr pfnCompletionRoutine = default, IntPtr lpArgToCompletionRoutine = default, bool fResume = false); [DllImport("kernel32.dll", EntryPoint = "OpenWaitableTimerW", SetLastError = true)] internal static extern Handle_ OpenWaitableTimer(uint dwDesiredAccess, bool bInheritHandle, string lpTimerName); [DllImport("kernel32.dll", EntryPoint = "GetModuleFileNameW", SetLastError = true)] internal static extern int GetModuleFileName(IntPtr hModule, char* lpFilename, int nSize); internal const uint PIPE_ACCESS_INBOUND = 0x1; internal const uint PIPE_ACCESS_OUTBOUND = 0x2; internal const uint PIPE_ACCESS_DUPLEX = 0x3; internal const uint PIPE_TYPE_MESSAGE = 0x4; internal const uint PIPE_READMODE_MESSAGE = 0x2; internal const uint PIPE_REJECT_REMOTE_CLIENTS = 0x8; [DllImport("kernel32.dll", EntryPoint = "CreateNamedPipeW", SetLastError = true)] static extern IntPtr _CreateNamedPipe(string lpName, uint dwOpenMode, uint dwPipeMode, uint nMaxInstances, uint nOutBufferSize, uint nInBufferSize, uint nDefaultTimeOut, SECURITY_ATTRIBUTES lpSecurityAttributes); internal static Handle_ CreateNamedPipe(string lpName, uint dwOpenMode, uint dwPipeMode, uint nMaxInstances, uint nOutBufferSize, uint nInBufferSize, uint nDefaultTimeOut, SECURITY_ATTRIBUTES lpSecurityAttributes) => new Handle_(_CreateNamedPipe(lpName, dwOpenMode, dwPipeMode, nMaxInstances, nOutBufferSize, nInBufferSize, nDefaultTimeOut, lpSecurityAttributes)); //note: cannot return Handle_ directly from API because returns -1 if failed. The ctor then makes 0. [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool CreatePipe(out Handle_ hReadPipe, out Handle_ hWritePipe, SECURITY_ATTRIBUTES lpPipeAttributes, uint nSize); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool ConnectNamedPipe(IntPtr hNamedPipe, OVERLAPPED* lpOverlapped); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool DisconnectNamedPipe(IntPtr hNamedPipe); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool GetOverlappedResult(IntPtr hFile, ref OVERLAPPED lpOverlapped, out int lpNumberOfBytesTransferred, bool bWait); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool CancelIo(IntPtr hFile); [DllImport("kernel32.dll", EntryPoint = "WaitNamedPipeW", SetLastError = true)] internal static extern bool WaitNamedPipe(string lpNamedPipeName, int nTimeOut); //[DllImport("kernel32.dll", EntryPoint = "CallNamedPipeW", SetLastError = true)] //internal static extern bool CallNamedPipe(string lpNamedPipeName, void* lpInBuffer, int nInBufferSize, out int lpOutBuffer, int nOutBufferSize, out int lpBytesRead, int nTimeOut); //[DllImport("kernel32.dll", SetLastError = true)] //internal static extern bool GetNamedPipeClientProcessId(IntPtr Pipe, out int ClientProcessId); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool PeekNamedPipe(IntPtr hNamedPipe, void* lpBuffer, int nBufferSize, out int lpBytesRead, out int lpTotalBytesAvail, IntPtr lpBytesLeftThisMessage = default); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool AllocConsole(); [DllImport("kernel32.dll", EntryPoint = "OutputDebugStringW")] internal static extern void OutputDebugString(string lpOutputString); [DllImport("kernel32.dll")] internal static extern bool SetProcessWorkingSetSize(IntPtr hProcess, nint dwMinimumWorkingSetSize, nint dwMaximumWorkingSetSize); //internal struct PROCESS_MEMORY_COUNTERS //{ // public int cb; // public int PageFaultCount; // public nint PeakWorkingSetSize; // public nint WorkingSetSize; // public nint QuotaPeakPagedPoolUsage; // public nint QuotaPagedPoolUsage; // public nint QuotaPeakNonPagedPoolUsage; // public nint QuotaNonPagedPoolUsage; // public nint PagefileUsage; // public nint PeakPagefileUsage; //} //[DllImport("kernel32.dll", EntryPoint = "K32GetProcessMemoryInfo")] //internal static extern bool GetProcessMemoryInfo(IntPtr Process, ref PROCESS_MEMORY_COUNTERS ppsmemCounters, int cb); [DllImport("kernel32.dll", EntryPoint = "FindResourceW", SetLastError = true)] public static extern IntPtr FindResource(IntPtr hModule, nint lpName, nint lpType); internal const int RT_GROUP_ICON = 14; //internal const int STD_INPUT_HANDLE = -10; internal const int STD_OUTPUT_HANDLE = -11; [DllImport("kernel32.dll")] internal static extern nint GetStdHandle(int nStdHandle); [DllImport("kernel32.dll", SetLastError = true)] internal static extern uint GetConsoleOutputCP(); [DllImport("kernel32.dll")] internal static extern nint SetUnhandledExceptionFilter(nint _); [DllImport("kernel32.dll", SetLastError = true)] internal static extern uint QueueUserAPC(delegate* unmanaged pfnAPC, IntPtr hThread, GCHandle dwData); internal delegate void PAPCFUNC(nint Parameter); [DllImport("kernel32.dll", EntryPoint = "GetDriveTypeW")] internal static extern int GetDriveType(string lpRootPathName); /// /// Use this API instead of Directory.CreateSymbolicLink which has a bug: does not throw exception when fails (eg non-admin). /// Note: the API fails if non-admin. /// With flag 2 does not fail if enabled developer mode. /// It seems can be enabled for non-admin in gpedit.msc; not tested; google for more info. /// Somewhere found this info, but it's incorrect: "Windows 11 doesn’t require administrative privileges to create symbolic links". /// /// 1 - directory. [DllImport("kernel32.dll", EntryPoint = "CreateSymbolicLinkW", SetLastError = true)] [return: MarshalAs(UnmanagedType.U1)] //BOOLEAN internal static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, uint dwFlags); [DllImport("kernel32.dll")] internal static extern int GetACP(); [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool DeviceIoControl(IntPtr hDevice, int dwIoControlCode, void* lpInBuffer, int nInBufferSize, void* lpOutBuffer, int nOutBufferSize, out int lpBytesReturned, nint lpOverlapped = 0); internal const int IOCTL_STORAGE_QUERY_PROPERTY = 0x2D1400; internal struct STORAGE_PROPERTY_QUERY { public int PropertyId; public int QueryType; public byte AdditionalParameters; } internal struct DEVICE_SEEK_PENALTY_DESCRIPTOR { public uint Version; public uint Size; public byte IncursSeekPenalty; } [DllImport("kernel32.dll", EntryPoint = "GetVolumePathNameW", SetLastError = true)] internal static extern bool GetVolumePathName(string lpszFileName, char* lpszVolumePathName, int cchBufferLength); [DllImport("kernel32.dll", EntryPoint = "GetVolumeNameForVolumeMountPointW", SetLastError = true)] internal static extern bool GetVolumeNameForVolumeMountPoint(string lpszVolumeMountPoint, char* lpszVolumeName, int cchBufferLength); [DllImport("kernel32.dll", EntryPoint = "GetCommandLineW")] internal static extern char* GetCommandLine(); [DllImport("kernel32.dll")] internal static extern int WTSGetActiveConsoleSessionId(); #region undocumented internal delegate int CheckElevationEnabled(out int pResult); #endregion } ================================================ FILE: Au/Api/Api^user32.cs ================================================ namespace Au.Types; //#pragma warning disable 649 //field never assigned static unsafe partial class Api { [DllImport("user32.dll", EntryPoint = "SendMessageW", SetLastError = true)] internal static extern nint SendMessage(wnd hWnd, int msg, nint wParam, nint lParam); [DllImport("user32.dll", EntryPoint = "SendMessageTimeoutW", SetLastError = true)] internal static extern nint SendMessageTimeout(wnd hWnd, int Msg, nint wParam, nint lParam, SMTFlags flags, int uTimeout, out nint lpdwResult); [DllImport("user32.dll", EntryPoint = "SendNotifyMessageW", SetLastError = true)] internal static extern bool SendNotifyMessage(wnd hWnd, int Msg, nint wParam, nint lParam); [DllImport("user32.dll", EntryPoint = "PostMessageW", SetLastError = true)] internal static extern bool PostMessage(wnd hWnd, int Msg, nint wParam, nint lParam); [DllImport("user32.dll", EntryPoint = "PostThreadMessageW", SetLastError = true)] internal static extern bool PostThreadMessage(int idThread, int Msg, nint wParam, nint lParam); [DllImport("user32.dll", SetLastError = true)] static extern nint GetWindowLongW(wnd hWnd, int nIndex); [DllImport("user32.dll", SetLastError = true)] static extern nint GetWindowLongPtrW(wnd hWnd, int nIndex); //info: 32-bit user32.dll does not have GetWindowLongPtrW etc. In C++, in x86 config it is defined as GetWindowLongW etc. internal static nint GetWindowLongPtr(wnd w, int nIndex) => IntPtr.Size == 8 ? GetWindowLongPtrW(w, nIndex) : GetWindowLongW(w, nIndex); [DllImport("user32.dll", SetLastError = true)] static extern nint SetWindowLongW(wnd hWnd, int nIndex, nint dwNewLong); [DllImport("user32.dll", SetLastError = true)] static extern nint SetWindowLongPtrW(wnd hWnd, int nIndex, nint dwNewLong); internal static nint SetWindowLongPtr(wnd w, int nIndex, nint dwNewLong) => IntPtr.Size == 8 ? SetWindowLongPtrW(w, nIndex, dwNewLong) : SetWindowLongW(w, nIndex, dwNewLong); [DllImport("user32.dll", SetLastError = true)] static extern nint GetClassLongW(wnd hWnd, int nIndex); [DllImport("user32.dll", SetLastError = true)] static extern nint GetClassLongPtrW(wnd hWnd, int nIndex); internal static nint GetClassLongPtr(wnd w, int nIndex) => IntPtr.Size == 8 ? GetClassLongPtrW(w, nIndex) : GetClassLongW(w, nIndex); [DllImport("user32.dll", SetLastError = true)] static extern nint SetClassLongW(wnd hWnd, int nIndex, nint dwNewLong); [DllImport("user32.dll", SetLastError = true)] static extern nint SetClassLongPtrW(wnd hWnd, int nIndex, nint dwNewLong); internal static nint SetClassLongPtr(wnd w, int nIndex, nint dwNewLong) => IntPtr.Size == 8 ? SetClassLongPtrW(w, nIndex, dwNewLong) : SetClassLongW(w, nIndex, dwNewLong); [DllImport("user32.dll", EntryPoint = "GetClassNameW", SetLastError = true)] internal static extern int GetClassName(wnd hWnd, char* lpClassName, int nMaxCount); [DllImport("user32.dll", EntryPoint = "InternalGetWindowText", SetLastError = true)] internal static extern int InternalGetWindowText(wnd hWnd, char* pString, int cchMaxCount); [DllImport("user32.dll")] internal static extern bool IsWindow(wnd hWnd); [DllImport("user32.dll", SetLastError = true)] internal static extern bool IsWindowVisible(wnd hWnd); internal const int SW_HIDE = 0; internal const int SW_SHOWNORMAL = 1; internal const int SW_SHOWMINIMIZED = 2; internal const int SW_SHOWMAXIMIZED = 3; internal const int SW_SHOWNOACTIVATE = 4; //restores min/max window internal const int SW_SHOW = 5; internal const int SW_MINIMIZE = 6; internal const int SW_SHOWMINNOACTIVE = 7; internal const int SW_SHOWNA = 8; internal const int SW_RESTORE = 9; internal const int SW_SHOWDEFAULT = 10; internal const int SW_FORCEMINIMIZE = 11; [DllImport("user32.dll", SetLastError = true)] internal static extern void ShowWindow(wnd hWnd, int SW_X); //note: the return value does not say succeeded/failed. // It is non-zero if was visible, 0 if was hidden. // Declared void to avoid programming errors. [DllImport("user32.dll", SetLastError = true)] internal static extern bool IsWindowEnabled(wnd hWnd); [DllImport("user32.dll", SetLastError = true)] internal static extern void EnableWindow(wnd hWnd, bool bEnable); //note: the returns value does not say succeeded/failed. // It is non-zero if was disabled, 0 if was enabled. // Declared void to avoid programming errors. [DllImport("user32.dll", EntryPoint = "FindWindowExW", SetLastError = true)] internal static extern wnd _FindWindowEx(wnd hWndParent, wnd hWndChildAfter, string lpszClass, string lpszWindow); internal static wnd FindWindowEx(wnd wParent = default, wnd wAfter = default, string cn = null, string name = null) { //Windows 11 bug: FindWindow[Ex] occasionally returns 0 for an existing window. // Second or third call usually succeeds. // More often fails when there is some activity, eg when Visual Studio compiles/launches an app, or when opening/closing Notepad++. // GetLastError 0. // To reproduce, call every 2 ms, eg to find Notepad by classname. // EnumWindows does not fail. // On Win10 never noticed and cannot reproduce. // Maybe FindWindowEx uses an unsafe loop like with GetWindow. When Z order changes, skips part of windows. // FUTURE: remove this workaround when API fixed. Also in wn::FindWndEx. if (osVersion.minWin11) { for (int i = 5; --i >= 0;) { var w = _FindWindowEx(wParent, wAfter, cn, name); if (!w.Is0) return w; } return default; } else { return _FindWindowEx(wParent, wAfter, cn, name); } } internal struct WNDCLASSEX { public int cbSize; public uint style; public IntPtr lpfnWndProc; //not WNDPROC to avoid auto-marshaling where don't need. Use Marshal.GetFunctionPointerForDelegate/GetDelegateForFunctionPointer. public int cbClsExtra; public int cbWndExtra; public IntPtr hInstance; public IntPtr hIcon; public IntPtr hCursor; public nint hbrBackground; #pragma warning disable 169 IntPtr lpszMenuName; #pragma warning restore 169 public char* lpszClassName; //not string because CLR would call CoTaskMemFree public IntPtr hIconSm; /// /// If ex null, sets arrow cursor and style CS_VREDRAW | CS_HREDRAW. /// public WNDCLASSEX(RWCEtc ex) { cbSize = sizeof(WNDCLASSEX); if (ex == null) { hCursor = LoadCursor(default, MCursor.Arrow); style = CS_VREDRAW | CS_HREDRAW; } else { style = ex.style; cbClsExtra = ex.cbClsExtra; cbWndExtra = ex.cbWndExtra; //hInstance = ex.hInstance; hIcon = ex.hIcon; if (ex.hCursor != default) hCursor = ex.hCursor; else if (ex.mCursor != default) hCursor = LoadCursor(default, ex.mCursor); hbrBackground = ex.hbrBackground; hIconSm = ex.hIconSm; } } } [DllImport("user32.dll", SetLastError = true)] internal static extern ushort RegisterClassEx(in WNDCLASSEX lpwcx); [DllImport("user32.dll", EntryPoint = "GetClassInfoExW", SetLastError = true)] internal static extern ushort GetClassInfoEx(IntPtr hInstance, string lpszClass, ref WNDCLASSEX lpwcx); [DllImport("user32.dll", EntryPoint = "UnregisterClassW", SetLastError = true)] internal static extern bool UnregisterClass(string lpClassName, IntPtr hInstance); [DllImport("user32.dll", EntryPoint = "UnregisterClassW", SetLastError = true)] internal static extern bool UnregisterClass(uint classAtom, IntPtr hInstance); [DllImport("user32.dll", EntryPoint = "CreateWindowExW", SetLastError = true)] internal static extern wnd CreateWindowEx(WSE dwExStyle, string lpClassName, string lpWindowName, WS dwStyle, int x, int y, int nWidth, int nHeight, wnd hWndParent = default, nint hMenu = 0, IntPtr hInstance = default, nint lpParam = 0); internal const int CW_USEDEFAULT = 1 << 31; [DllImport("user32.dll", EntryPoint = "DefWindowProcW")] internal static extern nint DefWindowProc(wnd hWnd, int msg, nint wParam, nint lParam); [DllImport("user32.dll", EntryPoint = "CallWindowProcW")] internal static extern nint CallWindowProc(nint lpPrevWndFunc, wnd hWnd, int Msg, nint wParam, nint lParam); [DllImport("user32.dll", SetLastError = true)] internal static extern bool DestroyWindow(wnd hWnd); [DllImport("user32.dll", SetLastError = true)] internal static extern void PostQuitMessage(int nExitCode); [DllImport("user32.dll", EntryPoint = "GetMessage", SetLastError = true)] internal static extern int _GetMessage(out MSG lpMsg, wnd hWnd, int wMsgFilterMin, int wMsgFilterMax); internal static bool GetMessage(out MSG m, wnd hWnd = default, int wMsgFilterMin = 0, int wMsgFilterMax = 0) { int r = _GetMessage(out m, hWnd, wMsgFilterMin, wMsgFilterMax); if (r == -1) throw new Win32Exception(); return r != 0; } [DllImport("user32.dll", SetLastError = true)] internal static extern bool TranslateMessage(in MSG lpMsg); [DllImport("user32.dll", SetLastError = true)] internal static extern nint DispatchMessage(in MSG lpmsg); internal const uint PM_NOREMOVE = 0x0; internal const uint PM_REMOVE = 0x1; internal const uint PM_NOYIELD = 0x2; internal const uint PM_QS_SENDMESSAGE = 0x400000; internal const uint PM_QS_POSTMESSAGE = 0x980000; internal const uint PM_QS_PAINT = 0x200000; internal const uint PM_QS_INPUT = 0x1C070000; [DllImport("user32.dll", EntryPoint = "PeekMessageW", SetLastError = true)] internal static extern bool PeekMessage(out MSG lpMsg, wnd hWnd = default, int wMsgFilterMin = 0, int wMsgFilterMax = 0, uint wRemoveMsg = PM_REMOVE); [DllImport("user32.dll")] internal static extern bool WaitMessage(); [DllImport("user32.dll", SetLastError = true)] internal static extern bool ReplyMessage(nint lResult); internal const int GA_PARENT = 1; internal const int GA_ROOT = 2; internal const int GA_ROOTOWNER = 3; [DllImport("user32.dll", SetLastError = true)] internal static extern wnd GetAncestor(wnd hwnd, uint GA_X); [DllImport("user32.dll")] internal static extern wnd GetForegroundWindow(); [DllImport("user32.dll", SetLastError = true)] internal static extern bool SetForegroundWindow(wnd hWnd); internal const int ASFW_ANY = -1; #if true [DllImport("user32.dll", EntryPoint = "AllowSetForegroundWindow", SetLastError = true)] static extern bool _AllowSetForegroundWindow(int dwProcessId); internal static bool AllowSetForegroundWindow(int dwProcessId = ASFW_ANY) { return _AllowSetForegroundWindow(ASFW_ANY); //Ignore dwProcessId, because AllowSetForegroundWindow doc says: //"The process specified by the dwProcessId parameter loses the ability to set the foreground window the next time that either the user generates input, unless the input is directed at that process, or the next time a process calls AllowSetForegroundWindow, unless the same process is specified as in the previous call to AllowSetForegroundWindow." //If it's true (not tested), if we call AllowSetForegroundWindow with a process id, we disable setforegroundwindow in all other processes. } #else [DllImport("user32.dll", SetLastError = true)] internal static extern bool AllowSetForegroundWindow(int dwProcessId = ASFW_ANY); #endif internal const uint LSFW_LOCK = 1; internal const uint LSFW_UNLOCK = 2; [DllImport("user32.dll", SetLastError = true)] internal static extern bool LockSetForegroundWindow(uint LSFW_X); [DllImport("user32.dll", SetLastError = true)] internal static extern wnd SetFocus(wnd hWnd); [DllImport("user32.dll")] internal static extern wnd GetFocus(); [DllImport("user32.dll", SetLastError = true)] internal static extern wnd SetActiveWindow(wnd hWnd); [DllImport("user32.dll")] internal static extern wnd GetActiveWindow(); internal struct WINDOWPOS { public wnd hwnd; public wnd hwndInsertAfter; public int x; public int y; public int cx; public int cy; public SWPFlags flags; } [DllImport("user32.dll", SetLastError = true)] internal static extern bool SetWindowPos(wnd hWnd, wnd hWndInsertAfter, int X, int Y, int cx, int cy, SWPFlags swpFlags); [DllImport("user32.dll", SetLastError = true)] internal static extern IntPtr BeginDeferWindowPos(int nNumWindows); [DllImport("user32.dll", SetLastError = true)] internal static extern IntPtr DeferWindowPos(IntPtr hWinPosInfo, wnd hWnd, wnd hWndInsertAfter, int x, int y, int cx, int cy, SWPFlags uFlags); [DllImport("user32.dll", SetLastError = true)] internal static extern bool EndDeferWindowPos(IntPtr hWinPosInfo); internal struct FLASHWINFO { public int cbSize; public wnd hwnd; public uint dwFlags; public int uCount; public int dwTimeout; } [DllImport("user32.dll", SetLastError = true)] internal static extern bool FlashWindowEx(ref FLASHWINFO pfwi); internal const int GW_OWNER = 4; internal const int GW_HWNDPREV = 3; internal const int GW_HWNDNEXT = 2; internal const int GW_HWNDLAST = 1; internal const int GW_HWNDFIRST = 0; internal const int GW_ENABLEDPOPUP = 6; internal const int GW_CHILD = 5; [DllImport("user32.dll", SetLastError = true)] internal static extern wnd GetWindow(wnd hWnd, int GW_X); [DllImport("user32.dll", SetLastError = true)] internal static extern wnd GetTopWindow(wnd hWnd); //rejected. Obsolete, confusing. // Returns owner for top-level windows with WS_POPUP style. // Returns 0 for child windows without WS_CHILD style, eg message-only windows and QM2 toolbar owners. //[DllImport("user32.dll", SetLastError = true)] //internal static extern wnd GetParent(wnd hWnd); [DllImport("user32.dll")] internal static extern wnd GetDesktopWindow(); [DllImport("user32.dll", SetLastError = true)] internal static extern wnd GetShellWindow(); [DllImport("user32.dll", SetLastError = true)] internal static extern wnd GetLastActivePopup(wnd hWnd); [DllImport("user32.dll")] internal static extern bool IntersectRect(out RECT lprcDst, in RECT lprcSrc1, in RECT lprcSrc2); [DllImport("user32.dll")] internal static extern bool UnionRect(out RECT lprcDst, in RECT lprcSrc1, in RECT lprcSrc2); /// /// GetPhysicalCursorPos. /// /// /// Gets DPI physical cursor pos, ie always in pixels. /// The classic GetCursorPos API behavior is undefined. Sometimes physical, sometimes logical. /// Make sure the process is fully DPI-aware. /// [DllImport("user32.dll", EntryPoint = "GetPhysicalCursorPos", SetLastError = true)] internal static extern bool GetCursorPos(out POINT lpPoint); [DllImport("user32.dll", SetLastError = true)] internal static extern bool SetCursorPos(int X, int Y); [DllImport("user32.dll", EntryPoint = "LoadImageW", SetLastError = true)] internal static extern IntPtr LoadImage(IntPtr hInst, string name, int type, int cx, int cy, uint LR_X); [DllImport("user32.dll", EntryPoint = "LoadImageW", SetLastError = true)] internal static extern IntPtr LoadImage(IntPtr hInst, nint resId, int type, int cx, int cy, uint LR_X); [DllImport("user32.dll", SetLastError = true)] internal static extern IntPtr CopyImage(IntPtr h, int type, int cx, int cy, uint flags); [DllImport("user32.dll", SetLastError = true)] internal static extern bool DestroyIcon(IntPtr hIcon); [DllImport("user32.dll", SetLastError = true)] internal static extern bool GetWindowRect(wnd hWnd, out RECT lpRect); [DllImport("user32.dll", SetLastError = true)] internal static extern bool GetClientRect(wnd hWnd, out RECT lpRect); internal const uint WPF_SETMINPOSITION = 0x1; internal const uint WPF_RESTORETOMAXIMIZED = 0x2; internal const uint WPF_ASYNCWINDOWPLACEMENT = 0x4; internal struct WINDOWPLACEMENT { public int length; /// WPF_x public uint flags; public int showCmd; public POINT ptMinPosition; public POINT ptMaxPosition; public RECT rcNormalPosition; } [DllImport("user32.dll", SetLastError = true)] internal static extern bool GetWindowPlacement(wnd hWnd, ref WINDOWPLACEMENT lpwndpl); [DllImport("user32.dll", SetLastError = true)] internal static extern bool SetWindowPlacement(wnd hWnd, in WINDOWPLACEMENT lpwndpl); internal struct WINDOWINFO { public int cbSize; public RECT rcWindow; public RECT rcClient; public WS dwStyle; public WSE dwExStyle; public uint dwWindowStatus; public int cxWindowBorders; public int cyWindowBorders; public ushort atomWindowType; public ushort wCreatorVersion; } [DllImport("user32.dll", SetLastError = true)] internal static extern bool GetWindowInfo(wnd hwnd, ref WINDOWINFO pwi); //[DllImport("user32.dll", SetLastError = true)] //internal static extern bool GetTitleBarInfo(wnd hwnd, ref TITLEBARINFO pti); //internal struct TITLEBARINFO { // public int cbSize; // public RECT r; // public fixed uint rgstate[6]; //} [DllImport("user32.dll", SetLastError = true)] internal static extern bool IsZoomed(wnd hWnd); [DllImport("user32.dll", SetLastError = true)] internal static extern bool IsIconic(wnd hWnd); [DllImport("user32.dll", SetLastError = true)] internal static extern int GetWindowThreadProcessId(wnd hWnd, int* lpdwProcessId); [DllImport("user32.dll", SetLastError = true)] internal static extern bool IsWindowUnicode(wnd hWnd); [DllImport("user32.dll", EntryPoint = "GetPropW", SetLastError = true)] internal static extern nint GetProp(wnd hWnd, string lpString); [DllImport("user32.dll", EntryPoint = "GetPropW", SetLastError = true)] //internal static extern nint GetProp(wnd hWnd, [MarshalAs(UnmanagedType.SysInt)] ushort atom); //exception, must be U2 internal static extern nint GetProp(wnd hWnd, nint atom); [DllImport("user32.dll", EntryPoint = "SetPropW", SetLastError = true)] internal static extern bool SetProp(wnd hWnd, string lpString, nint hData); [DllImport("user32.dll", EntryPoint = "SetPropW", SetLastError = true)] internal static extern bool SetProp(wnd hWnd, nint atom, nint hData); [DllImport("user32.dll", EntryPoint = "RemovePropW", SetLastError = true)] internal static extern nint RemoveProp(wnd hWnd, string lpString); [DllImport("user32.dll", EntryPoint = "RemovePropW", SetLastError = true)] internal static extern nint RemoveProp(wnd hWnd, nint atom); internal delegate bool PROPENUMPROCEX(wnd hwnd, IntPtr lpszString, nint hData, nint dwData); [DllImport("user32.dll", EntryPoint = "EnumPropsExW", SetLastError = true)] internal static extern int EnumPropsEx(wnd hWnd, PROPENUMPROCEX lpEnumFunc, nint lParam); [DllImport("user32.dll", SetLastError = true)] internal static extern wnd GetDlgItem(wnd hDlg, int nIDDlgItem); [DllImport("user32.dll", SetLastError = true)] internal static extern int GetDlgCtrlID(wnd hWnd); internal delegate int WNDENUMPROC(wnd w, void* p); [DllImport("user32.dll", SetLastError = true)] internal static extern bool EnumWindows(WNDENUMPROC lpEnumFunc, void* p = null); [DllImport("user32.dll", SetLastError = true)] internal static extern bool EnumThreadWindows(int dwThreadId, WNDENUMPROC lpEnumFunc, void* p = null); [DllImport("user32.dll", SetLastError = true)] internal static extern bool EnumChildWindows(wnd hWndParent, WNDENUMPROC lpEnumFunc, void* p = null); [DllImport("user32.dll", EntryPoint = "RegisterWindowMessageW", SetLastError = true)] internal static extern int RegisterWindowMessage(string lpString); [DllImport("user32.dll", SetLastError = true)] internal static extern bool IsChild(wnd hWndParent, wnd hWnd); //rejected. As slow as GetAncestor(GA_PARENT). //[DllImport("user32.dll", SetLastError = true), Obsolete("Undocumented API")] //internal static extern bool IsTopLevelWindow(wnd hWnd); [DllImport("user32.dll")] internal static extern IntPtr MonitorFromPoint(POINT pt, SODefault dwFlags); [DllImport("user32.dll")] internal static extern IntPtr MonitorFromRect(in RECT lprc, SODefault dwFlags); [DllImport("user32.dll")] internal static extern IntPtr MonitorFromWindow(wnd hwnd, SODefault dwFlags); internal struct MONITORINFO { public int cbSize; public RECT rcMonitor; public RECT rcWork; public uint dwFlags; } [DllImport("user32.dll", EntryPoint = "GetMonitorInfoW")] static extern bool _GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi); internal static bool GetMonitorInfo(IntPtr hMonitor, out MONITORINFO lpmi) { lpmi = new MONITORINFO { cbSize = sizeof(MONITORINFO) }; return _GetMonitorInfo(hMonitor, ref lpmi); } [DllImport("user32.dll", SetLastError = true)] internal static extern bool EnumDisplayMonitors(nint hdc, RECT* lprcClip, delegate* unmanaged lpfnEnum, nint* a); #region GetSystemMetrics, SystemParametersInfo internal const int SM_YVIRTUALSCREEN = 77; internal const int SM_XVIRTUALSCREEN = 76; internal const int SM_TABLETPC = 86; internal const int SM_SWAPBUTTON = 23; internal const int SM_STARTER = 88; internal const int SM_SLOWMACHINE = 73; internal const int SM_SHUTTINGDOWN = 8192; internal const int SM_SHOWSOUNDS = 70; internal const int SM_SERVERR2 = 89; internal const int SM_SECURE = 44; internal const int SM_SAMEDISPLAYFORMAT = 81; internal const int SM_RESERVED4 = 27; internal const int SM_RESERVED3 = 26; internal const int SM_RESERVED2 = 25; internal const int SM_RESERVED1 = 24; internal const int SM_REMOTESESSION = 4096; internal const int SM_REMOTECONTROL = 8193; internal const int SM_PENWINDOWS = 41; internal const int SM_NETWORK = 63; internal const int SM_MOUSEWHEELPRESENT = 75; internal const int SM_MOUSEPRESENT = 19; internal const int SM_MIDEASTENABLED = 74; internal const int SM_MENUDROPALIGNMENT = 40; internal const int SM_MEDIACENTER = 87; internal const int SM_IMMENABLED = 82; internal const int SM_DEBUG = 22; internal const int SM_DBCSENABLED = 42; internal const int SM_CYVTHUMB = 9; internal const int SM_CYVSCROLL = 20; internal const int SM_CYVIRTUALSCREEN = 79; internal const int SM_CYSMSIZE = 53; internal const int SM_CYSMICON = 50; internal const int SM_CYSMCAPTION = 51; internal const int SM_CYSIZEFRAME = SM_CYFRAME; internal const int SM_CYSIZE = 31; internal const int SM_CYSCREEN = 1; internal const int SM_CYMINTRACK = 35; internal const int SM_CYMINSPACING = 48; internal const int SM_CYMINIMIZED = 58; internal const int SM_CYMIN = 29; internal const int SM_CYMENUSIZE = 55; internal const int SM_CYMENUCHECK = 72; internal const int SM_CYMENU = 15; internal const int SM_CYMAXTRACK = 60; internal const int SM_CYMAXIMIZED = 62; internal const int SM_CYKANJIWINDOW = 18; internal const int SM_CYICONSPACING = 39; internal const int SM_CYICON = 12; internal const int SM_CYHSCROLL = 3; internal const int SM_CYFULLSCREEN = 17; internal const int SM_CYFRAME = 33; internal const int SM_CYFOCUSBORDER = 84; internal const int SM_CYFIXEDFRAME = SM_CYDLGFRAME; internal const int SM_CYEDGE = 46; internal const int SM_CYDRAG = 69; internal const int SM_CYDOUBLECLK = 37; internal const int SM_CYDLGFRAME = 8; internal const int SM_CYCURSOR = 14; internal const int SM_CYCAPTION = 4; internal const int SM_CYBORDER = 6; internal const int SM_CXVSCROLL = 2; internal const int SM_CXVIRTUALSCREEN = 78; internal const int SM_CXSMSIZE = 52; internal const int SM_CXSMICON = 49; internal const int SM_CXSIZEFRAME = SM_CXFRAME; internal const int SM_CXSIZE = 30; internal const int SM_CXSCREEN = 0; internal const int SM_CXMINTRACK = 34; internal const int SM_CXMINSPACING = 47; internal const int SM_CXMINIMIZED = 57; internal const int SM_CXMIN = 28; internal const int SM_CXMENUSIZE = 54; internal const int SM_CXMENUCHECK = 71; internal const int SM_CXMAXTRACK = 59; internal const int SM_CXMAXIMIZED = 61; internal const int SM_CXICONSPACING = 38; internal const int SM_CXICON = 11; internal const int SM_CXHTHUMB = 10; internal const int SM_CXHSCROLL = 21; internal const int SM_CXFULLSCREEN = 16; internal const int SM_CXFRAME = 32; internal const int SM_CXFOCUSBORDER = 83; internal const int SM_CXFIXEDFRAME = SM_CXDLGFRAME; internal const int SM_CXEDGE = 45; internal const int SM_CXDRAG = 68; internal const int SM_CXDOUBLECLK = 36; internal const int SM_CXDLGFRAME = 7; internal const int SM_CXCURSOR = 13; internal const int SM_CXBORDER = 5; internal const int SM_CMOUSEBUTTONS = 43; internal const int SM_CMONITORS = 80; internal const int SM_CMETRICS = 90; internal const int SM_CLEANBOOT = 67; internal const int SM_CARETBLINKINGENABLED = 8194; internal const int SM_ARRANGE = 56; [DllImport("user32.dll", SetLastError = true)] internal static extern int GetSystemMetrics(int nIndex); [DllImport("user32.dll", SetLastError = true)] internal static extern int GetSystemMetricsForDpi(int nIndex, int dpi); internal const int SPI_SETWORKAREA = 47; internal const int SPI_SETWHEELSCROLLLINES = 105; internal const int SPI_SETUIEFFECTS = 4159; internal const int SPI_SETTOOLTIPFADE = 4121; internal const int SPI_SETTOOLTIPANIMATION = 4119; internal const int SPI_SETTOGGLEKEYS = 53; internal const int SPI_SETSTICKYKEYS = 59; internal const int SPI_SETSOUNDSENTRY = 65; internal const int SPI_SETSNAPTODEFBUTTON = 96; internal const int SPI_SETSHOWSOUNDS = 57; internal const int SPI_SETSHOWIMEUI = 111; internal const int SPI_SETSERIALKEYS = 63; internal const int SPI_SETSELECTIONFADE = 4117; internal const int SPI_SETSCREENSAVETIMEOUT = 15; internal const int SPI_SETSCREENSAVERRUNNING = 97; internal const int SPI_SETSCREENSAVEACTIVE = 17; internal const int SPI_SETSCREENREADER = 71; internal const int SPI_SETPOWEROFFTIMEOUT = 82; internal const int SPI_SETPOWEROFFACTIVE = 86; internal const int SPI_SETPENWINDOWS = 49; internal const int SPI_SETNONCLIENTMETRICS = 42; internal const int SPI_SETMOUSEVANISH = 4129; internal const int SPI_SETMOUSETRAILS = 93; internal const int SPI_SETMOUSESPEED = 113; internal const int SPI_SETMOUSESONAR = 4125; internal const int SPI_SETMOUSEKEYS = 55; internal const int SPI_SETMOUSEHOVERWIDTH = 99; internal const int SPI_SETMOUSEHOVERTIME = 103; internal const int SPI_SETMOUSEHOVERHEIGHT = 101; internal const int SPI_SETMOUSECLICKLOCKTIME = 8201; internal const int SPI_SETMOUSECLICKLOCK = 4127; internal const int SPI_SETMOUSEBUTTONSWAP = 33; internal const int SPI_SETMOUSE = 4; internal const int SPI_SETMINIMIZEDMETRICS = 44; internal const int SPI_SETMENUUNDERLINES = SPI_SETKEYBOARDCUES; internal const int SPI_SETMENUSHOWDELAY = 107; internal const int SPI_SETMENUFADE = 4115; internal const int SPI_SETMENUDROPALIGNMENT = 28; internal const int SPI_SETMENUANIMATION = 4099; internal const int SPI_SETLOWPOWERTIMEOUT = 81; internal const int SPI_SETLOWPOWERACTIVE = 85; internal const int SPI_SETLISTBOXSMOOTHSCROLLING = 4103; internal const int SPI_SETLANGTOGGLE = 91; internal const int SPI_SETKEYBOARDSPEED = 11; internal const int SPI_SETKEYBOARDPREF = 69; internal const int SPI_SETKEYBOARDDELAY = 23; internal const int SPI_SETKEYBOARDCUES = 4107; internal const int SPI_SETICONTITLEWRAP = 26; internal const int SPI_SETICONTITLELOGFONT = 34; internal const int SPI_SETICONS = 88; internal const int SPI_SETICONMETRICS = 46; internal const int SPI_SETHOTTRACKING = 4111; internal const int SPI_SETHIGHCONTRAST = 67; internal const int SPI_SETHANDHELD = 78; internal const int SPI_SETGRIDGRANULARITY = 19; internal const int SPI_SETGRADIENTCAPTIONS = 4105; internal const int SPI_SETFOREGROUNDLOCKTIMEOUT = 8193; internal const int SPI_SETFOREGROUNDFLASHCOUNT = 8197; internal const int SPI_SETFONTSMOOTHINGTYPE = 8203; internal const int SPI_SETFONTSMOOTHINGORIENTATION = 8211; internal const int SPI_SETFONTSMOOTHINGCONTRAST = 8205; internal const int SPI_SETFONTSMOOTHING = 75; internal const int SPI_SETFOCUSBORDERWIDTH = 8207; internal const int SPI_SETFOCUSBORDERHEIGHT = 8209; internal const int SPI_SETFLATMENU = 4131; internal const int SPI_SETFILTERKEYS = 51; internal const int SPI_SETFASTTASKSWITCH = 36; internal const int SPI_SETDROPSHADOW = 4133; internal const int SPI_SETDRAGWIDTH = 76; internal const int SPI_SETDRAGHEIGHT = 77; internal const int SPI_SETDRAGFULLWINDOWS = 37; internal const int SPI_SETDOUBLECLKWIDTH = 29; internal const int SPI_SETDOUBLECLKHEIGHT = 30; internal const int SPI_SETDOUBLECLICKTIME = 32; internal const int SPI_SETDESKWALLPAPER = 20; internal const int SPI_SETDESKPATTERN = 21; internal const int SPI_SETDEFAULTINPUTLANG = 90; internal const int SPI_SETCURSORSHADOW = 4123; internal const int SPI_SETCURSORS = 87; internal const int SPI_SETCOMBOBOXANIMATION = 4101; internal const int SPI_SETCARETWIDTH = 8199; internal const int SPI_SETBORDER = 6; internal const int SPI_SETBLOCKSENDINPUTRESETS = 4135; internal const int SPI_SETBEEP = 2; internal const int SPI_SETANIMATION = 73; internal const int SPI_SETACTIVEWNDTRKZORDER = 4109; internal const int SPI_SETACTIVEWNDTRKTIMEOUT = 8195; internal const int SPI_SETACTIVEWINDOWTRACKING = 4097; internal const int SPI_SETACCESSTIMEOUT = 61; internal const int SPI_LANGDRIVER = 12; internal const int SPI_ICONVERTICALSPACING = 24; internal const int SPI_ICONHORIZONTALSPACING = 13; internal const int SPI_GETWORKAREA = 48; internal const int SPI_GETWINDOWSEXTENSION = 92; internal const int SPI_GETWHEELSCROLLLINES = 104; internal const int SPI_GETUIEFFECTS = 4158; internal const int SPI_GETTOOLTIPFADE = 4120; internal const int SPI_GETTOOLTIPANIMATION = 4118; internal const int SPI_GETTOGGLEKEYS = 52; internal const int SPI_GETSTICKYKEYS = 58; internal const int SPI_GETSOUNDSENTRY = 64; internal const int SPI_GETSNAPTODEFBUTTON = 95; internal const int SPI_GETSHOWSOUNDS = 56; internal const int SPI_GETSHOWIMEUI = 110; internal const int SPI_GETSERIALKEYS = 62; internal const int SPI_GETSELECTIONFADE = 4116; internal const int SPI_GETSCREENSAVETIMEOUT = 14; internal const int SPI_GETSCREENSAVERRUNNING = 114; internal const int SPI_GETSCREENSAVEACTIVE = 16; internal const int SPI_GETSCREENREADER = 70; internal const int SPI_GETPOWEROFFTIMEOUT = 80; internal const int SPI_GETPOWEROFFACTIVE = 84; internal const int SPI_GETNONCLIENTMETRICS = 41; internal const int SPI_GETMOUSEVANISH = 4128; internal const int SPI_GETMOUSETRAILS = 94; internal const int SPI_GETMOUSESPEED = 112; internal const int SPI_GETMOUSESONAR = 4124; internal const int SPI_GETMOUSEKEYS = 54; internal const int SPI_GETMOUSEHOVERWIDTH = 98; internal const int SPI_GETMOUSEHOVERTIME = 102; internal const int SPI_GETMOUSEHOVERHEIGHT = 100; internal const int SPI_GETMOUSECLICKLOCKTIME = 8200; internal const int SPI_GETMOUSECLICKLOCK = 4126; internal const int SPI_GETMOUSE = 3; internal const int SPI_GETMINIMIZEDMETRICS = 43; internal const int SPI_GETMENUUNDERLINES = SPI_GETKEYBOARDCUES; internal const int SPI_GETMENUSHOWDELAY = 106; internal const int SPI_GETMENUFADE = 4114; internal const int SPI_GETMENUDROPALIGNMENT = 27; internal const int SPI_GETMENUANIMATION = 4098; internal const int SPI_GETLOWPOWERTIMEOUT = 79; internal const int SPI_GETLOWPOWERACTIVE = 83; internal const int SPI_GETLISTBOXSMOOTHSCROLLING = 4102; internal const int SPI_GETKEYBOARDSPEED = 10; internal const int SPI_GETKEYBOARDPREF = 68; internal const int SPI_GETKEYBOARDDELAY = 22; internal const int SPI_GETKEYBOARDCUES = 4106; internal const int SPI_GETICONTITLEWRAP = 25; internal const int SPI_GETICONTITLELOGFONT = 31; internal const int SPI_GETICONMETRICS = 45; internal const int SPI_GETHOTTRACKING = 4110; internal const int SPI_GETHIGHCONTRAST = 66; internal const int SPI_GETGRIDGRANULARITY = 18; internal const int SPI_GETGRADIENTCAPTIONS = 4104; internal const int SPI_GETFOREGROUNDLOCKTIMEOUT = 8192; internal const int SPI_GETFOREGROUNDFLASHCOUNT = 8196; internal const int SPI_GETFONTSMOOTHINGTYPE = 8202; internal const int SPI_GETFONTSMOOTHINGORIENTATION = 8210; internal const int SPI_GETFONTSMOOTHINGCONTRAST = 8204; internal const int SPI_GETFONTSMOOTHING = 74; internal const int SPI_GETFOCUSBORDERWIDTH = 8206; internal const int SPI_GETFOCUSBORDERHEIGHT = 8208; internal const int SPI_GETFLATMENU = 4130; internal const int SPI_GETFILTERKEYS = 50; internal const int SPI_GETFASTTASKSWITCH = 35; internal const int SPI_GETDROPSHADOW = 4132; internal const int SPI_GETDRAGFULLWINDOWS = 38; internal const int SPI_GETDESKWALLPAPER = 115; internal const int SPI_GETDEFAULTINPUTLANG = 89; internal const int SPI_GETCURSORSHADOW = 4122; internal const int SPI_GETCOMBOBOXANIMATION = 4100; internal const int SPI_GETCARETWIDTH = 8198; internal const int SPI_GETBORDER = 5; internal const int SPI_GETBLOCKSENDINPUTRESETS = 4134; internal const int SPI_GETBEEP = 1; internal const int SPI_GETANIMATION = 72; internal const int SPI_GETACTIVEWNDTRKZORDER = 4108; internal const int SPI_GETACTIVEWNDTRKTIMEOUT = 8194; internal const int SPI_GETACTIVEWINDOWTRACKING = 4096; internal const int SPI_GETACCESSTIMEOUT = 60; internal const int SPI_GETMOUSEWHEELROUTING = 0x201C; internal const uint SPIF_UPDATEINIFILE = 0x1; internal const uint SPIF_SENDCHANGE = 0x2; /// /// Gets or sets any value. This is the direct API call. /// [DllImport("user32.dll", EntryPoint = "SystemParametersInfoW", SetLastError = true)] internal static extern bool SystemParametersInfo(uint uiAction, int uiParam, void* pvParam, uint fWinIni = 0); /// /// Gets 32-bit integer value. Returns def if failed. /// internal static int SystemParametersInfo(uint uiAction, int def) { int r = 0; return SystemParametersInfo(uiAction, 0, &r) ? r : def; } /// /// Gets BOOL value. Returns false if failed. /// internal static bool SystemParametersInfo(uint uiAction) { int r = 0; return SystemParametersInfo(uiAction, 0, &r) && r != 0; } //internal static bool SystemParametersInfo(uint uiAction, ref T pvParam) where T : unmanaged { // fixed (T* p = &pvParam) return SystemParametersInfo(uiAction, sizeof(T), p, 0); //} /// /// Sets value. /// internal static bool SystemParametersInfo(uint uiAction, int uiParam, void* pvParam, bool save, bool notify) { uint f = 0; if (save) f |= 1; //SPIF_UPDATEINIFILE if (notify) f |= 2; //SPIF_SENDCHANGE return SystemParametersInfo(uiAction, uiParam, pvParam, f); } [DllImport("user32.dll", SetLastError = true)] internal static extern bool SystemParametersInfoForDpi(uint uiAction, int uiParam, void* pvParam, uint fWinIni, int dpi); #endregion /// /// WindowFromPhysicalPoint. On Win8.1+ it is the same as WindowFromPoint. /// [DllImport("user32.dll", EntryPoint = "WindowFromPhysicalPoint")] internal static extern wnd WindowFromPoint(POINT pt); [DllImport("user32.dll", SetLastError = true)] internal static extern bool ScreenToClient(wnd hWnd, ref POINT lpPoint); [DllImport("user32.dll", SetLastError = true)] internal static extern bool ClientToScreen(wnd hWnd, ref POINT lpPoint); //internal static bool ClientToScreenIgnoreRtl(wnd w, ref POINT p) //{ // if(!w.HasExStyle(WSE.LAYOUTRTL)) return ClientToScreen(w, ref p); // if(!GetClientRect(w, out var r) || !MapWindowPoints(w, default, ref r, out _)) return false; // p.Offset(r.left, r.top); // return true; //} [DllImport("user32.dll", SetLastError = true)] static extern int MapWindowPoints(wnd hWndFrom, wnd hWndTo, void* lpPoints, int cPoints); internal static bool MapWindowPoints(wnd wFrom, wnd wTo, void* points, int cPoints, out int ret) { lastError.clear(); ret = MapWindowPoints(wFrom, wTo, points, cPoints); return ret != 0 ? true : lastError.code == 0; } internal static bool MapWindowPoints(wnd wFrom, wnd wTo, ref RECT r, out int ret) { fixed (void* u = &r) return MapWindowPoints(wFrom, wTo, u, 2, out ret); } internal static bool MapWindowPoints(wnd wFrom, wnd wTo, ref POINT p, out int ret) { fixed (void* u = &p) return MapWindowPoints(wFrom, wTo, u, 1, out ret); } [DllImport("user32.dll", SetLastError = true)] internal static extern bool GetGUIThreadInfo(int idThread, ref GUITHREADINFO pgui); [DllImport("user32.dll", SetLastError = true)] internal static extern bool AttachThreadInput(int idAttach, int idAttachTo, bool fAttach); internal const uint KEYEVENTF_EXTENDEDKEY = 0x1; internal const uint KEYEVENTF_KEYUP = 0x2; internal const uint KEYEVENTF_UNICODE = 0x4; internal const uint KEYEVENTF_SCANCODE = 0x8; internal struct INPUTK { nint _type; public ushort wVk; public ushort wScan; public uint dwFlags; public int time; public nint dwExtraInfo; #pragma warning disable 414 //never used int _u1, _u2; //need INPUT size #pragma warning restore 414 public INPUTK(KKey vk, ushort sc, uint flags = 0) { _type = INPUT_KEYBOARD; dwExtraInfo = AuExtraInfo; wVk = (ushort)vk; wScan = sc; dwFlags = flags; time = 0; _u2 = _u1 = 0; Debug.Assert(sizeof(INPUTK) == sizeof(INPUTM)); } public void Set(KKey vk, ushort sc, uint flags = 0) { _type = INPUT_KEYBOARD; dwExtraInfo = AuExtraInfo; wVk = (ushort)vk; wScan = sc; dwFlags = flags; } //public void InitCommonFields() //{ // _type = INPUT_KEYBOARD; dwExtraInfo = AuExtraInfo; //} const int INPUT_KEYBOARD = 1; } [Flags] internal enum IMFlags : uint { Move = 1, LeftDown = 2, LeftUp = 4, RightDown = 8, RightUp = 16, MiddleDown = 32, MiddleUp = 64, XDown = 0x80, XUp = 0x100, Wheel = 0x0800, HWheel = 0x01000, NoCoalesce = 0x2000, VirtualdDesktop = 0x4000, Absolute = 0x8000, //not API X1 = 0x1000000, X2 = 0x2000000, }; internal struct INPUTM { nint _type; public int dx; public int dy; public int mouseData; public IMFlags dwFlags; public int time; public nint dwExtraInfo; public INPUTM(IMFlags flags, int x = 0, int y = 0, int data = 0) { _type = INPUT_MOUSE; dx = x; dy = y; dwFlags = flags; mouseData = data; time = 0; dwExtraInfo = AuExtraInfo; } const int INPUT_MOUSE = 0; } /// /// Extra info value of key and mouse events sent by functions of this library. /// internal const int AuExtraInfo = 0x71427fa5; [DllImport("user32.dll", SetLastError = true)] static extern int SendInput(int cInputs, void* pInputs, int cbSize); //tested: Returns 0 when: invalid argument; other desktop active. // Does not return 0 when: UAC (documented); BlockInput; ClipCursor. /// internal static void SendInput(INPUTK* ip, int n = 1, bool dontThrow = false) { if (n != SendInput(n, ip, sizeof(INPUTK))) { if (!dontThrow) InputDesktopException.ThrowIfBadDesktop("*send keyboard input."); //tested: if bad input desktop, GetLastError returns 'access denied'. //throw new AuException("*send keyboard input."); //rejected. Anyway in most cases cannot detect when fails (because API returns not 0). Debug_.Print($"SendInput(key) failed. {lastError.message}"); } } /// internal static void SendInput(INPUTM* ip, int n = 1, bool dontThrow = false) { if (n != SendInput(n, ip, sizeof(INPUTM))) { if (!dontThrow) InputDesktopException.ThrowIfBadDesktop("*send mouse input."); //throw new AuException("*send mouse input."); Debug_.Print($"SendInput(mouse) failed. {lastError.message}"); } } [DllImport("user32.dll", SetLastError = true)] internal static extern bool IsHungAppWindow(wnd hwnd); [DllImport("user32.dll", SetLastError = true)] internal static extern bool SetLayeredWindowAttributes(wnd hwnd, uint crKey, byte bAlpha, uint dwFlags); [DllImport("user32.dll", SetLastError = true)] internal static extern bool GetLayeredWindowAttributes(wnd hwnd, out uint pcrKey, out byte pbAlpha, out uint pdwFlags); [DllImport("user32.dll", SetLastError = true)] internal static extern IntPtr CreateIcon(IntPtr hInstance, int nWidth, int nHeight, byte cPlanes, byte cBitsPixel, byte[] lpbANDbits, byte[] lpbXORbits); [DllImport("user32.dll", EntryPoint = "LoadCursorW", SetLastError = true)] internal static extern IntPtr LoadCursor(IntPtr hInstance, MCursor cursorId); internal delegate void TIMERPROC(wnd param1, int param2, nint param3, uint param4); [DllImport("user32.dll", SetLastError = true)] internal static extern nint SetTimer(wnd hWnd, nint nIDEvent, int uElapse, TIMERPROC lpTimerFunc); [DllImport("user32.dll", SetLastError = true)] internal static extern bool KillTimer(wnd hWnd, nint uIDEvent); [DllImport("user32.dll", SetLastError = true)] internal static extern wnd SetParent(wnd hWndChild, wnd hWndNewParent); [DllImport("user32.dll", SetLastError = true)] internal static extern bool AdjustWindowRectEx(ref RECT lpRect, WS dwStyle, bool bMenu, WSE dwExStyle); [DllImport("user32.dll", SetLastError = true)] internal static extern bool AdjustWindowRectExForDpi(ref RECT lpRect, WS dwStyle, bool bMenu, WSE dwExStyle, int dpi); [DllImport("user32.dll", SetLastError = true)] internal static extern bool ChangeWindowMessageFilter(int message, uint dwFlag); [DllImport("user32.dll", SetLastError = true)] internal static extern short GetKeyState(int nVirtKey); //not KKey because it is :byte [DllImport("user32.dll", SetLastError = true)] internal static extern short GetAsyncKeyState(int vKey); //not KKey because it is :byte internal const uint MOD_ALT = 0x1; internal const uint MOD_CONTROL = 0x2; internal const uint MOD_SHIFT = 0x4; internal const uint MOD_WIN = 0x8; internal const uint MOD_NOREPEAT = 0x4000; [DllImport("user32.dll", SetLastError = true)] internal static extern bool RegisterHotKey(wnd hWnd, int id, uint fsModifiers, KKey vk); [DllImport("user32.dll", SetLastError = true)] internal static extern bool UnregisterHotKey(wnd hWnd, int id); internal const uint MWMO_WAITALL = 0x1; internal const uint MWMO_ALERTABLE = 0x2; internal const uint MWMO_INPUTAVAILABLE = 0x4; [DllImport("user32.dll", SetLastError = true)] internal static extern int MsgWaitForMultipleObjectsEx(int nCount, IntPtr* pHandles, int dwMilliseconds, uint dwWakeMask, uint MWMO_Flags); [DllImport("user32.dll", SetLastError = true)] internal static extern bool InvalidateRect(wnd hWnd, RECT* lpRect, bool bErase); internal static bool InvalidateRect(wnd hWnd, bool bErase = false) => InvalidateRect(hWnd, null, bErase); internal static bool InvalidateRect(wnd hWnd, RECT r, bool bErase = false) => InvalidateRect(hWnd, &r, bErase); [DllImport("user32.dll", SetLastError = true)] internal static extern bool ValidateRect(wnd hWnd, RECT* lpRect); internal static bool ValidateRect(wnd hWnd) => ValidateRect(hWnd, null); [DllImport("user32.dll", SetLastError = true)] internal static extern bool GetUpdateRect(wnd hWnd, out RECT lpRect, bool bErase); internal const int ERROR = 0; internal const int NULLREGION = 1; internal const int SIMPLEREGION = 2; internal const int COMPLEXREGION = 3; [DllImport("user32.dll", SetLastError = true)] internal static extern int GetUpdateRgn(wnd hWnd, IntPtr hRgn, bool bErase); [DllImport("user32.dll", SetLastError = true)] internal static extern bool InvalidateRgn(wnd hWnd, IntPtr hRgn, bool bErase); [DllImport("user32.dll", SetLastError = true)] internal static extern bool DragDetect(wnd hwnd, POINT pt); [DllImport("user32.dll")] internal static extern IntPtr GetCursor(); [DllImport("user32.dll", SetLastError = true)] internal static extern IntPtr SetCursor(IntPtr hCursor); [DllImport("user32.dll", SetLastError = true)] internal static extern wnd SetCapture(wnd hWnd); [DllImport("user32.dll")] internal static extern wnd GetCapture(); [DllImport("user32.dll")] internal static extern bool ReleaseCapture(); //[DllImport("user32.dll", EntryPoint = "CharLowerBuffW")] //internal static unsafe extern int CharLowerBuff(char* lpsz, int cchLength); //[DllImport("user32.dll", CallingConvention = CallingConvention.Cdecl)] //internal static extern int wsprintfW(char* lpOut1024, string lpFmt, __arglist); //note: with __arglist always returns 0. Could instead use void*, but then much work to properly pack arguments. //tested speed (time %) of various formatting functions, with two int and one string arg, with converting to string: // StringBuilder.Append + int.ToString(CultureInfo.InvariantCulture): 85% - FASTEST // StringBuilder.Append: 100% // StringBuilder.AppendFormat + int.ToString() (avoid int boxing): 140% // StringBuilder.AppendFormat: 150% // $"{var} string": 160% (probably uses StringBuilder) // wsprintfW (user32.dll): 150% // _snwprintf (msvcrt.dll): 500% - SLOWEST [DllImport("user32.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] internal static extern int wsprintfA(byte* lpOut1024, string lpFmt, __arglist); [DllImport("user32.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern int wsprintfW(char* lpOut1024, string lpFmt, __arglist); internal struct PAINTSTRUCT { public IntPtr hdc; public bool fErase; public RECT rcPaint; public bool fRestore; public bool fIncUpdate; fixed byte rgbReserved[32]; } [DllImport("user32.dll")] internal static extern IntPtr BeginPaint(wnd hWnd, out PAINTSTRUCT lpPaint); [DllImport("user32.dll")] internal static extern bool EndPaint(wnd hWnd, in PAINTSTRUCT lpPaint); [DllImport("user32.dll")] internal static extern bool UpdateWindow(wnd hWnd); [DllImport("user32.dll")] internal static extern nint GetKeyboardLayout(int idThread); [DllImport("user32.dll", EntryPoint = "MapVirtualKeyExW")] internal static extern uint MapVirtualKeyEx(uint uCode, uint uMapType, nint dwhkl); [DllImport("user32.dll", EntryPoint = "VkKeyScanExW")] internal static extern short VkKeyScanEx(char ch, nint dwhkl); [DllImport("user32.dll", SetLastError = true)] internal static extern bool OpenClipboard(wnd hWndNewOwner); [DllImport("user32.dll", SetLastError = true)] internal static extern bool CloseClipboard(); [DllImport("user32.dll", SetLastError = true)] internal static extern bool EmptyClipboard(); [DllImport("user32.dll", SetLastError = true)] internal static extern IntPtr SetClipboardData(int uFormat, IntPtr hMem); [DllImport("user32.dll", SetLastError = true)] internal static extern IntPtr GetClipboardData(int uFormat); //[DllImport("user32.dll", SetLastError = true)] //internal static extern wnd SetClipboardViewer(wnd hWndNewViewer); //[DllImport("user32.dll")] //internal static extern bool ChangeClipboardChain(wnd hWndRemove, wnd hWndNewNext); [DllImport("user32.dll")] internal static extern wnd GetOpenClipboardWindow(); [DllImport("user32.dll")] internal static extern uint GetClipboardSequenceNumber(); [DllImport("user32.dll", EntryPoint = "RegisterClipboardFormatW")] internal static extern int RegisterClipboardFormat(string lpszFormat); [DllImport("user32.dll", SetLastError = true)] internal static extern bool AddClipboardFormatListener(wnd hwnd); [DllImport("user32.dll", SetLastError = true)] internal static extern bool RemoveClipboardFormatListener(wnd hwnd); [DllImport("user32.dll", SetLastError = true)] internal static extern int EnumClipboardFormats(int format); [DllImport("user32.dll", SetLastError = true)] internal static extern bool IsClipboardFormatAvailable(int format); [DllImport("user32.dll", SetLastError = true)] internal static extern int GetPriorityClipboardFormat(int[] paFormatPriorityList, int cFormats); [DllImport("user32.dll", EntryPoint = "GetClipboardFormatNameW", SetLastError = true)] internal static unsafe extern int GetClipboardFormatName(int format, char* lpszFormatName, int cchMaxCount); [DllImport("user32.dll")] internal static extern int GetDoubleClickTime(); [DllImport("user32.dll", SetLastError = true)] internal static extern bool DrawIconEx(IntPtr hdc, int xLeft, int yTop, IntPtr hIcon, int cxWidth, int cyWidth, int istepIfAniCur = 0, IntPtr hbrFlickerFreeDraw = default, uint diFlags = 3); //DI_NORMAL internal const uint CURSOR_SHOWING = 0x1; internal struct CURSORINFO { public int cbSize; public uint flags; public IntPtr hCursor; public POINT ptScreenPos; } [DllImport("user32.dll", SetLastError = true)] internal static extern bool GetCursorInfo(ref CURSORINFO pci); internal struct ICONINFO : IDisposable { public bool fIcon; public int xHotspot; public int yHotspot; public IntPtr hbmMask; public IntPtr hbmColor; public ICONINFO(IntPtr hIcon) { GetIconInfo(hIcon, out this); //never mind if failed. Then hbm members are default, and caller can either check it or simply let other API fail. } public void Dispose() { if (hbmMask != default) DeleteObject(hbmMask); if (hbmColor != default) DeleteObject(hbmColor); } } [DllImport("user32.dll", SetLastError = true)] internal static extern bool GetIconInfo(IntPtr hIcon, out ICONINFO piconinfo); //tested: GetIconInfoEx gets resource info only for icons loaded from a module loaded in this process. internal struct BITMAP { public int bmType; public int bmWidth; public int bmHeight; public int bmWidthBytes; public ushort bmPlanes; public ushort bmBitsPixel; public IntPtr bmBits; } internal const int WH_MSGFILTER = -1; internal const int WH_KEYBOARD = 2; internal const int WH_GETMESSAGE = 3; internal const int WH_CALLWNDPROC = 4; internal const int WH_CBT = 5; //internal const int WH_SYSMSGFILTER = 6; //hook proc must be in dll internal const int WH_MOUSE = 7; internal const int WH_DEBUG = 9; internal const int WH_SHELL = 10; internal const int WH_FOREGROUNDIDLE = 11; internal const int WH_CALLWNDPROCRET = 12; internal const int WH_KEYBOARD_LL = 13; internal const int WH_MOUSE_LL = 14; internal delegate nint HOOKPROC(int code, nint wParam, nint lParam); [DllImport("user32.dll", SetLastError = true)] internal static extern IntPtr SetWindowsHookEx(int WH_X, HOOKPROC lpfn, IntPtr hMod, int dwThreadId); [DllImport("user32.dll", SetLastError = true)] internal static extern bool UnhookWindowsHookEx(IntPtr hhk); [DllImport("user32.dll", SetLastError = true)] internal static extern nint CallNextHookEx(IntPtr hhk, int nCode, nint wParam, nint lParam); internal const uint LLKHF_EXTENDED = 0x1; internal const uint LLKHF_INJECTED = 0x10; internal const uint LLKHF_ALTDOWN = 0x20; internal const uint LLKHF_UP = 0x80; internal struct KBDLLHOOKSTRUCT { public uint vkCode; public uint scanCode; public uint flags; public int time; public nint dwExtraInfo; public bool IsUp => 0 != (flags & LLKHF_UP); /// /// true if the event was generated by software. /// public bool IsInjected => 0 != (flags & LLKHF_INJECTED); /// /// true if the event was generated by functions of this library. /// public bool IsInjectedByAu => 0 != (flags & LLKHF_INJECTED) && dwExtraInfo == AuExtraInfo; //CONSIDER: also add IsInjectedByAuOrQM2. And let QM2 recognize the extra info too. Or let they use the same extra info; probably bad idea. /// /// The set function adds or removes flag 0x80000000. /// The get function returns true if flag 0x80000000 is set. /// public bool BlockEvent { get => 0 != (flags & 0x80000000); set { if (value) flags |= 0x80000000; else flags &= ~0x80000000; } } } internal const uint LLMHF_INJECTED = 0x1; internal struct MSLLHOOKSTRUCT { public POINT pt; public uint mouseData; public uint flags; public int time; public nint dwExtraInfo; /// /// true if the event was generated by software. /// public bool IsInjected => 0 != (flags & LLMHF_INJECTED); /// /// true if the event was generated by functions of this library. /// public bool IsInjectedByAu => 0 != (flags & LLMHF_INJECTED) && dwExtraInfo == AuExtraInfo; /// /// The set function adds or removes flag 0x80000000. /// The get function returns true if flag 0x80000000 is set. /// public bool BlockEvent { get => 0 != (flags & 0x80000000); set { if (value) flags |= 0x80000000; else flags &= ~0x80000000; } } } internal const int HC_NOREMOVE = 3; internal delegate void WINEVENTPROC(IntPtr hWinEventHook, EEvent event_, wnd hwnd, EObjid idObject, int idChild, int idEventThread, int dwmsEventTime); [DllImport("user32.dll", SetLastError = true)] internal static extern IntPtr SetWinEventHook(EEvent eventMin, EEvent eventMax, IntPtr hmodWinEventProc, WINEVENTPROC pfnWinEventProc, int idProcess, int idThread, EHookFlags dwFlags); [DllImport("user32.dll", SetLastError = true)] internal static extern bool UnhookWinEvent(IntPtr hWinEventHook); [Flags] internal enum AnimationFlags : uint { Roll = 0x0000, // Uses a roll animation. HorizontalPositive = 0x00001, // Animates the window from left to right. This flag can be used with roll or slide animation. HorizontalNegative = 0x00002, // Animates the window from right to left. This flag can be used with roll or slide animation. VerticalPositive = 0x00004, // Animates the window from top to bottom. This flag can be used with roll or slide animation. VerticalNegative = 0x00008, // Animates the window from bottom to top. This flag can be used with roll or slide animation. Center = 0x00010, // Makes the window appear to collapse inward if Hide is used or expand outward if the Hide is not used. Hide = 0x10000, // Hides the window. By default, the window is shown. Activate = 0x20000, // Activates the window. Slide = 0x40000, // Uses a slide animation. By default, roll animation is used. Blend = 0x80000, // Uses a fade effect. This flag can be used only with a top-level window. Mask = 0xfffff, } [DllImport("user32.dll", SetLastError = true)] internal static extern bool AnimateWindow(wnd hWnd, int dwTime, AnimationFlags dwFlags); [DllImport("user32.dll", SetLastError = true)] internal static extern bool GetCaretPos(out POINT lpPoint); internal static bool GetCaretPosInScreen_(out POINT p) => GetCaretPos(out p) && GetFocus().MapClientToScreen(ref p); [DllImport("user32.dll")] internal static extern int ToUnicodeEx(uint wVirtKey, uint wScanCode, byte* lpKeyState, char* pwszBuff, int cchBuff, uint wFlags, IntPtr dwhkl); internal const uint PW_CLIENTONLY = 0x1; internal const uint PW_RENDERFULLCONTENT = 0x2; [DllImport("user32.dll", SetLastError = true)] internal static extern bool PrintWindow(wnd hwnd, IntPtr hdcBlt, uint nFlags); [DllImport("user32.dll", SetLastError = true)] internal static extern IntPtr GetDC(wnd hWnd); [DllImport("user32.dll", SetLastError = true)] internal static extern IntPtr GetWindowDC(wnd hWnd); [DllImport("user32.dll")] //note: no SetLastError = true internal static extern int ReleaseDC(wnd hWnd, IntPtr hDC); [DllImport("user32.dll")] internal static extern int FillRect(IntPtr hDC, in RECT lprc, nint hbr); [DllImport("user32.dll")] internal static extern int FrameRect(IntPtr hDC, in RECT lprc, IntPtr hbr); internal const uint RDW_INVALIDATE = 0x1; internal const uint RDW_ERASE = 0x4; internal const uint RDW_ALLCHILDREN = 0x80; internal const uint RDW_FRAME = 0x400; [DllImport("user32.dll")] internal static extern bool RedrawWindow(wnd hWnd, RECT* lprcUpdate = null, IntPtr hrgnUpdate = default, uint flags = 0); /// Au.Controls.PopupAlignment [DllImport("user32.dll", SetLastError = true)] internal static extern bool CalculatePopupWindowPosition(in POINT anchorPoint, in SIZE windowSize, uint flags, in RECT excludeRect, out RECT popupWindowPosition); [DllImport("user32.dll")] internal static extern int MenuItemFromPoint(wnd hWnd, IntPtr hMenu, POINT ptScreen); [DllImport("user32.dll")] internal static extern int GetMenuItemID(IntPtr hMenu, int nPos); internal struct MENUITEMINFO { public int cbSize; public uint fMask; public uint fType; public uint fState; public int wID; public IntPtr hSubMenu; public IntPtr hbmpChecked; public IntPtr hbmpUnchecked; public nint dwItemData; public char* dwTypeData; public int cch; public IntPtr hbmpItem; public MENUITEMINFO(uint miim) { cbSize = sizeof(MENUITEMINFO); fMask = miim; } } [DllImport("user32.dll", EntryPoint = "GetMenuItemInfoW")] internal static extern bool GetMenuItemInfo(IntPtr hmenu, int item, bool fByPosition, ref MENUITEMINFO lpmii); internal const uint MIIM_STRING = 0x40; [DllImport("user32.dll")] internal static extern IntPtr GetSystemMenu(wnd hWnd, bool bRevert); [DllImport("user32.dll")] internal static extern bool EnableMenuItem(IntPtr hMenu, uint uIDEnableItem, uint uEnable); internal const uint MF_GRAYED = 0x1; internal const uint SIF_RANGE = 0x1; internal const uint SIF_PAGE = 0x2; internal const uint SIF_POS = 0x4; internal const uint SIF_TRACKPOS = 0x10; internal const int SB_LINEUP = 0; //internal const int SB_LINELEFT = 0; internal const int SB_LINEDOWN = 1; //internal const int SB_LINERIGHT = 1; internal const int SB_PAGEUP = 2; //internal const int SB_PAGELEFT = 2; internal const int SB_PAGEDOWN = 3; //internal const int SB_PAGERIGHT = 3; //internal const int SB_THUMBPOSITION = 4; internal const int SB_THUMBTRACK = 5; internal const int SB_TOP = 6; //internal const int SB_LEFT = 6; internal const int SB_BOTTOM = 7; //internal const int SB_RIGHT = 7; //internal const int SB_ENDSCROLL = 8; internal const int SB_HORZ = 0; internal const int SB_VERT = 1; //internal const int SB_CTL = 2; internal const int SB_BOTH = 3; internal struct SCROLLINFO { public int cbSize; public uint fMask; public int nMin; public int nMax; public int nPage; public int nPos; public int nTrackPos; public SCROLLINFO(uint mask) { cbSize = sizeof(SCROLLINFO); fMask = mask; } public int Set(wnd w, bool vertical, bool redraw = true) => SetScrollInfo(w, vertical ? SB_VERT : SB_HORZ, this, redraw); public bool Get(wnd w, bool vertical) => GetScrollInfo(w, vertical ? SB_VERT : SB_HORZ, ref this); public static SCROLLINFO Get(wnd w, bool vertical, uint mask) { SCROLLINFO v = new(mask); v.Get(w, vertical); return v; } public static int GetTrackPos(wnd w, bool vertical) => Get(w, vertical, SIF_TRACKPOS).nTrackPos; public static void SetPos(wnd w, bool vertical, int pos, bool redraw = true) { new SCROLLINFO(SIF_POS) { nPos = pos }.Set(w, vertical, redraw); } public static void SetRange(wnd w, bool vertical, int max, int page, bool redraw = true) { new SCROLLINFO(SIF_RANGE | SIF_PAGE) { nMax = max, nPage = page }.Set(w, vertical, redraw); } } [DllImport("user32.dll")] internal static extern int SetScrollInfo(wnd hwnd, int nBar, in SCROLLINFO lpsi, bool redraw); [DllImport("user32.dll")] internal static extern bool GetScrollInfo(wnd hwnd, int nBar, ref SCROLLINFO lpsi); [DllImport("user32.dll")] internal static extern bool ShowScrollBar(wnd hWnd, int wBar, bool bShow); //[DllImport("user32.dll", SetLastError = true)] //internal static extern bool GetScrollBarInfo(wnd hwnd, EObjid idObject, ref SCROLLBARINFO psbi); //internal struct SCROLLBARINFO //{ // public int cbSize; // public RECT rcScrollBar; // public int dxyLineButton; // public int xyThumbTop; // public int xyThumbBottom; // public int reserved; // //public fixed uint rgstate[6]; // public uint stateScrollbar, stateArrowTopRight, statePageUpRight, stateThumb, statePageDownLeft, stateArrowBottomLeft; //} //internal const uint STATE_SYSTEM_INVISIBLE = 0x8000; //internal const uint STATE_SYSTEM_OFFSCREEN = 0x10000; //internal const uint STATE_SYSTEM_PRESSED = 0x8; //internal const uint STATE_SYSTEM_UNAVAILABLE = 0x1; [DllImport("user32.dll", EntryPoint = "MessageBoxW")] internal static extern int MessageBox(wnd hWnd, string lpText, string lpCaption, uint uType); [DllImport("user32.dll", SetLastError = true)] internal static extern int GetWindowRgn(wnd hWnd, IntPtr hRgn); [DllImport("user32.dll", SetLastError = true)] internal static extern int GetDpiForWindow(wnd hWnd); [DllImport("user32.dll")] internal static extern IntPtr GetWindowDpiAwarenessContext(wnd hwnd); [DllImport("user32.dll")] internal static extern Dpi.Awareness GetAwarenessFromDpiAwarenessContext(IntPtr value); [DllImport("user32.dll", SetLastError = true)] internal static extern nint SetThreadDpiAwarenessContext(nint dpiContext); [DllImport("shcore.dll")] internal static extern int GetDpiForMonitor(IntPtr hmonitor, int dpiType, out int dpiX, out int dpiY); [DllImport("shcore.dll")] internal static extern int GetProcessDpiAwareness(IntPtr hprocess, out Dpi.Awareness value); //Dpi.Awareness is PROCESS_DPI_AWARENESS [DllImport("user32.dll")] internal static extern bool PhysicalToLogicalPointForPerMonitorDPI(wnd hWnd, ref POINT lpPoint); //[DllImport("user32.dll")] //internal static extern bool PhysicalToLogicalPoint(wnd hWnd, ref POINT lpPoint); //internal static bool PhysicalToLogicalPoint_AnyOS(wnd w, ref POINT p) => osVersion.minWin8_1 ? PhysicalToLogicalPointForPerMonitorDPI(w, ref p) : PhysicalToLogicalPoint(w, ref p); //[DllImport("user32.dll")] //internal static extern bool LogicalToPhysicalPointForPerMonitorDPI(wnd hWnd, ref POINT lpPoint); [DllImport("user32.dll")] internal static extern bool LogicalToPhysicalPoint(wnd hWnd, ref POINT lpPoint); //internal static bool LogicalToPhysicalPoint_AnyOS(wnd w, ref POINT p) => osVersion.minWin8_1 ? LogicalToPhysicalPointForPerMonitorDPI(w, ref p) : LogicalToPhysicalPoint(w, ref p); [DllImport("user32.dll")] internal static extern int GetSysColor(int nIndex); [DllImport("user32.dll")] internal static extern IntPtr GetSysColorBrush(int nIndex); //internal struct DRAWTEXTPARAMS //{ // public int cbSize; // public int iTabLength; // public int iLeftMargin; // public int iRightMargin; // public int uiLengthDrawn; //} //[DllImport("user32.dll", SetLastError = true)] //static extern int DrawTextExW(IntPtr hdc, char* lpchText, int cchText, ref RECT lprc, TFFlags format, DRAWTEXTPARAMS* lpdtp); [DllImport("user32.dll", SetLastError = true)] static extern int DrawTextExW(IntPtr hdc, char* lpchText, int cchText, ref RECT lprc, TFFlags format, void* lpdtp); internal static int DrawText(IntPtr hdc, RStr s, ref RECT lprc, TFFlags format) { if (format.Has(TFFlags.MODIFYSTRING)) throw new NotSupportedException("MODIFYSTRING"); fixed (char* p = s) return DrawTextExW(hdc, p, s.Length, ref lprc, format, null); //DRAWTEXTPARAMS doc incorrect. Left nad right margin fields are in pixels, not average char widths. Not tested tab width. } internal const WS TTS_ALWAYSTIP = (WS)0x1; internal const WS TTS_NOPREFIX = (WS)0x2; internal const WS TTS_BALLOON = (WS)0x40; internal const int TTM_ACTIVATE = 0x401; internal const int TTM_SETMAXTIPWIDTH = 0x418; internal const int TTM_ADDTOOL = 0x432; internal const int TTM_DELTOOL = 0x433; internal const int TTM_RELAYEVENT = 0x407; //internal const uint TTF_SUBCLASS = 0x10; internal struct TTTOOLINFO { public int cbSize; public uint uFlags; public wnd hwnd; public nint uId; public RECT rect; public IntPtr hinst; public char* lpszText; public nint lParam; public void* lpReserved; } [DllImport("user32.dll")] internal static extern bool DrawEdge(IntPtr hdc, ref RECT qrc, uint edge, uint grfFlags); internal const uint EDGE_ETCHED = 0x6; internal const uint BF_LEFT = 0x1; internal const uint BF_TOP = 0x2; [DllImport("user32.dll", SetLastError = true)] internal static extern IntPtr GetThreadDesktop(int dwThreadId); [DllImport("user32.dll", EntryPoint = "GetUserObjectInformationW", SetLastError = true)] internal static extern bool GetUserObjectInformation(IntPtr hObj, int nIndex, void* pvInfo, int nLength, out int lpnLengthNeeded); internal const int UOI_IO = 6; //internal const int UOI_NAME = 2; //[DllImport("user32.dll", SetLastError = true)] //internal static extern IntPtr OpenInputDesktop(uint dwFlags, bool fInherit, uint dwDesiredAccess); //[DllImport("user32.dll")] //internal static extern bool CloseDesktop(IntPtr hDesktop); internal const uint ISMEX_SEND = 0x1; internal const uint ISMEX_REPLIED = 0x8; [DllImport("user32.dll")] internal static extern uint InSendMessageEx(nint lpReserved = 0); public static bool InSendMessageBlocked => (InSendMessageEx() & (ISMEX_SEND | ISMEX_REPLIED)) == ISMEX_SEND; [DllImport("user32.dll", SetLastError = true)] internal static extern bool ExitWindowsEx(int uFlags, uint dwReason); [DllImport("user32.dll", SetLastError = true)] internal static extern bool LockWorkStation(); [DllImport("user32.dll", SetLastError = true)] internal static extern nint RegisterSuspendResumeNotification(IntPtr hRecipient, uint Flags); internal struct DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS { public delegate* unmanaged Callback; public void* Context; } //internal const uint DEVICE_NOTIFY_WINDOW_HANDLE = 0x0; //internal const int DEVICE_NOTIFY_CALLBACK = 2; [DllImport("user32.dll")] internal static extern bool UnregisterSuspendResumeNotification(nint Handle); //internal struct DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS { // public IntPtr Callback; //DEVICE_NOTIFY_CALLBACK_ROUTINE // public nint Context; //} //internal delegate uint DEVICE_NOTIFY_CALLBACK_ROUTINE(nint context, uint type, nint setting); [DllImport("user32.dll", SetLastError = true)] internal static extern bool ClipCursor(RECT* lpRect); [DllImport("user32.dll")] static extern bool GetLastInputInfo(ref LASTINPUTINFO plii); struct LASTINPUTINFO { public int cbSize; public uint dwTime; } /// /// Calls API and makes the result 64-bit. /// internal static long GetLastInputTime() { var r = new LASTINPUTINFO { cbSize = 8 }; if (!GetLastInputInfo(ref r)) return 0; return TickCount32To64(r.dwTime); } /// /// Extends a 32-bit tick count (like from many Windows API) into the 64-bit tick count space. /// /// 32-bit time, like from API GetTickCount. Must not be in the future. internal static long TickCount32To64(uint tick32) { long now64 = Environment.TickCount64; long r = (now64 & ~0xffffffffL) | tick32; if (r > now64) r -= 0x1_0000_0000L; return r; } } ================================================ FILE: Au/Api/Api_COM.cs ================================================ namespace Au.Types; static unsafe partial class Api { internal struct STRRET { public uint uType; [StructLayout(LayoutKind.Explicit)] internal struct TYPE_1 { [FieldOffset(0)] public char* pOleStr; [FieldOffset(0)] public uint uOffset; [FieldOffset(0)] public fixed sbyte cStr[260]; } public TYPE_1 _2; } //internal static Guid IID_IShellFolder = new Guid(0x000214E6, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); [ComImport, Guid("000214E6-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IShellFolder { //[PreserveSig] int ParseDisplayName(wnd hwnd, IntPtr pbc, [MarshalAs(UnmanagedType.LPWStr)] string pszDisplayName, uint* pchEaten, out IntPtr ppidl, uint* pdwAttributes); //[PreserveSig] int EnumObjects(wnd hwnd, uint grfFlags, out IEnumIDList ppenumIDList); //[PreserveSig] int BindToObject(IntPtr pidl, IntPtr pbc, in Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppv); //[PreserveSig] int BindToStorage(IntPtr pidl, IntPtr pbc, in Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppv); //[PreserveSig] int CompareIDs(nint lParam, IntPtr pidl1, IntPtr pidl2); //[PreserveSig] int CreateViewObject(wnd hwndOwner, in Guid riid, out IntPtr ppv); //[PreserveSig] int GetAttributesOf(uint cidl, [MarshalAs(UnmanagedType.LPArray)] IntPtr[] apidl, ref uint rgfInOut); void _0(); void _1(); void _2(); void _3(); void _4(); void _5(); void _6(); [PreserveSig] int GetUIObjectOf(wnd hwndOwner, uint cidl, in IntPtr pidl, in Guid riid, nint rgfReserved, [MarshalAs(UnmanagedType.IUnknown)] out object ppv); //[PreserveSig] int GetDisplayNameOf(IntPtr pidl, uint uFlags, out STRRET pName); //[PreserveSig] int SetNameOf(wnd hwnd, IntPtr pidl, [MarshalAs(UnmanagedType.LPWStr)] string pszName, uint uFlags, out IntPtr ppidlOut); } internal static bool GetUIObjectOf(this IShellFolder t, IntPtr pidl, out T result) where T : class { result = null; if (0 != t.GetUIObjectOf(default, 1, pidl, typeof(T).GUID, 0, out var o) || o is not T r) return false; result = r; return true; } //internal static Guid IID_IShellItem = new Guid(0x43826D1E, 0xE718, 0x42EE, 0xBC, 0x55, 0xA1, 0xE2, 0x61, 0xC3, 0x7B, 0xFE); [ComImport, Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IShellItem { //[PreserveSig] int BindToHandler(IntPtr pbc, in Guid bhid, in Guid riid, out IntPtr ppv); //IBindCtx //[PreserveSig] int GetParent(out IShellItem ppsi); void _0(); void _1(); [PreserveSig] int GetDisplayName(SIGDN sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName); [PreserveSig] int GetAttributes(uint sfgaoMask, out uint psfgaoAttribs); //[PreserveSig] int Compare(IShellItem psi, uint hint, out int piOrder); } //[ComImport, Guid("000214F2-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] //internal interface IEnumIDList { // [PreserveSig] int Next(int celt, [MarshalAs(UnmanagedType.LPArray)][Out] IntPtr[] rgelt, out int pceltFetched); // [PreserveSig] int Skip(int celt); // [PreserveSig] int Reset(); // [PreserveSig] int Clone(out IEnumIDList ppenum); //} [ComImport, Guid("000214fa-0000-0000-c000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IExtractIcon { [PreserveSig] int GetIconLocation(uint uFlags, StringBuilder pszIconFile, int cchMax, out int piIndex, out uint pwFlags); //[PreserveSig] int Extract([MarshalAs(UnmanagedType.LPWStr)] string pszFile, int nIconIndex, out IntPtr phiconLarge, out IntPtr phiconSmall, uint nIconSize); } [ComImport, Guid("000214F9-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IShellLink { [PreserveSig] int GetPath(char* pszFile, int cch, IntPtr pfd = default, uint fFlags = 0); [PreserveSig] int GetIDList(out IntPtr ppidl); [PreserveSig] int SetIDList(IntPtr pidl); [PreserveSig] int GetDescription(char* pszName, int cch); [PreserveSig] int SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName); [PreserveSig] int GetWorkingDirectory(char* pszDir, int cch); [PreserveSig] int SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir); [PreserveSig] int GetArguments(char* pszArgs, int cch); [PreserveSig] int SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs); [PreserveSig] int GetHotkey(out ushort pwHotkey); [PreserveSig] int SetHotkey(ushort wHotkey); [PreserveSig] int GetShowCmd(out int piShowCmd); [PreserveSig] int SetShowCmd(int iShowCmd); [PreserveSig] int GetIconLocation(char* pszIconPath, int cch, out int piIcon); [PreserveSig] int SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon); [PreserveSig] int SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, uint dwReserved = 0); [PreserveSig] int Resolve(wnd hwnd, uint fFlags); [PreserveSig] int SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile); //info: default string marshaling in COM interfaces is BSTR, but in this interface strings are LPWSTR. Cannot use plain string and char[]. // Instead of [MarshalAs(UnmanagedType.LPArray)] [Out] char[] can be just char*. Then also need fixed when calling. } [ComImport, Guid("00021401-0000-0000-C000-000000000046"), ClassInterface(ClassInterfaceType.None)] internal class ShellLink { } [ComImport, Guid("0000010b-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IPersistFile { // IPersist [PreserveSig] int GetClassID(out Guid pClassID); // IPersistFile [PreserveSig] int IsDirty(); [PreserveSig] int Load([MarshalAs(UnmanagedType.LPWStr)] string pszFileName, uint dwMode); [PreserveSig] int Save([MarshalAs(UnmanagedType.LPWStr)] string pszFileName, [MarshalAs(UnmanagedType.Bool)] bool fRemember); //[PreserveSig] int SaveCompleted([MarshalAs(UnmanagedType.LPWStr)] string pszFileName); //[PreserveSig] int GetCurFile(out IntPtr ppszFileName); } //see also VARIANT in Struct.cs internal struct PROPVARIANT : IDisposable { public VARENUM vt; //ushort ushort _u1; uint _u2; public nint value; public nint value2; /// /// Calls PropVariantClear. /// public void Dispose() { PropVariantClear(ref this); } } internal struct PROPERTYKEY { public Guid fmtid; public uint pid; } internal static Guid IID_IPropertyStore = new Guid(0x886D8EEB, 0x8CF2, 0x4446, 0x8D, 0x02, 0xCD, 0xBA, 0x1D, 0xBD, 0xCF, 0x99); [ComImport, Guid("886d8eeb-8cf2-4446-8d02-cdba1dbdcf99"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IPropertyStore { [PreserveSig] int GetCount(out int cProps); [PreserveSig] int GetAt(int iProp, out PROPERTYKEY pkey); [PreserveSig] int GetValue(in PROPERTYKEY key, out PROPVARIANT pv); [PreserveSig] int SetValue(in PROPERTYKEY key, ref PROPVARIANT propvar); [PreserveSig] int Commit(); } //note: this is used in the lib, even if IImageList isn't. internal static Guid IID_IImageList = new Guid(0x46EB5926, 0x582E, 0x4017, 0x9F, 0xDF, 0xE8, 0x99, 0x8D, 0xAA, 0x09, 0x50); [ComImport, Guid("a5cd92ff-29be-454c-8d04-d82879fb3f1b"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IVirtualDesktopManager { [PreserveSig] int IsWindowOnCurrentVirtualDesktop(wnd topLevelWindow, [MarshalAs(UnmanagedType.Bool)] out bool onCurrentDesktop); [PreserveSig] int GetWindowDesktopId(wnd topLevelWindow, out Guid desktopId); [PreserveSig] int MoveWindowToDesktop(wnd topLevelWindow, in Guid desktopId); } [ComImport, Guid("aa509086-5ca9-4c25-8f95-589d3c07b48a"), ClassInterface(ClassInterfaceType.None)] internal class VirtualDesktopManager { } } ================================================ FILE: Au/Api/Api_UIA.cs ================================================ namespace Au.Types; /// /// Wraps some UI Automation API. /// static class UiaUtil { public static UiaApi.IUIAutomation Uia => _uia ??= osVersion.minWin8 ? new UiaApi.CUIAutomation8() as UiaApi.IUIAutomation : new UiaApi.CUIAutomation() as UiaApi.IUIAutomation; [ThreadStatic] static UiaApi.IUIAutomation _uia; /// /// Gets UI element from point. /// /// Screen coordinates. /// null if failed. public static UiaApi.IUIAutomationElement ElementFromPoint(POINT xy) { return 0 == Uia.ElementFromPoint(xy, out var e) ? e : null; } /// /// Gets the focused element. /// /// null if failed. public static UiaApi.IUIAutomationElement ElementFocused() { return 0 == Uia.GetFocusedElement(out var e) ? e : null; } ///// ///// Gets the container control of this or nearest ancestor element that can retrieve it. ///// //public static wnd Hwnd(this UiaApi.IUIAutomationElement t) { // if (0 == t.get_CurrentNativeWindowHandle(out var w) && !w.Is0) return w; // if (0 == Uia.get_RawViewWalker(out var walker)) { // while (0 == walker.GetParentElement(t, out var p) && p != null && p != t) { // t = p; // if (0 == t.get_CurrentNativeWindowHandle(out w) && !w.Is0) return w; // } // } // return default; //} /// /// Gets caret rectangle in screen from this focused element. /// public static bool GetCaretRect(this UiaApi.IUIAutomationElement t, out RECT r) { if (0 == t.GetCurrentPattern(UiaApi.UIA_TextPattern2Id, out var o) && o is UiaApi.IUIAutomationTextPattern2 p2) { if (0 == p2.GetCaretRange(out bool isActive, out var tr) && tr != null /*&& isActive*/) { return tr.GetRect(out r, t); } } if (0 == t.GetCurrentPattern(UiaApi.UIA_TextPatternId, out o) && o is UiaApi.IUIAutomationTextPattern p) { if (0 == p.GetSelection(out var ranges) && 0 == ranges.GetElement(0, out var tr)) { return tr.GetRect(out r, t, selectionToCaret: true); } } r = default; return false; } /// /// Gets rectangle in screen. /// public static unsafe bool GetRect(this UiaApi.IUIAutomationTextRange t, out RECT r, UiaApi.IUIAutomationElement e, bool selectionToCaret = false) { if (_GetRect(t, out r)) { if (selectionToCaret) r.left = r.right - 1; return true; } //probably no selection if (0 == t.ExpandToEnclosingUnit(UiaApi.TextUnit.TextUnit_Character) && _GetRect(t, out r)) { r.right = r.left + 1; return true; } //probably caret at the end if (0 == t.MoveEndpointByUnit(UiaApi.TextPatternRangeEndpoint.TextPatternRangeEndpoint_Start, UiaApi.TextUnit.TextUnit_Character, -1, out int m) && m < 0 && _GetRect(t, out r)) { //moved to previous line? if (0 == t.GetText(2, out var s) && !s.NE() && s[0] is '\r' or '\n' && 0 == t.ExpandToEnclosingUnit(UiaApi.TextUnit.TextUnit_Line) && _GetRect(t, out var r2)) { r.Offset(0, r2.Height); r.right = (r.left = r2.left) + 1; } else { r.right = (r.left = r.right) + 1; } return true; } //probably no text //get the left edge of e rect if (0 == e.get_CurrentBoundingRectangle(out r)) { int dpi = Dpi.OfWindow(wnd.active); int h = (int)Dpi.Unscale(r.Height, dpi); if (h < 111) { //assume it's a single-line edit control if (h > 32) r.top = r.bottom - Dpi.Scale(32, dpi); //get the bottom max 32 logical pixels r.right = r.left + 1; return true; } } return false; static unsafe bool _GetRect(UiaApi.IUIAutomationTextRange t, out RECT r) { r = default; if (0 != t.GetBoundingRectangles(out var sap) || sap == null) return false; uint n = sap->rgsabound.cElements / 4; if (n > 0) { var p = (double*)sap->pvData + sap->rgsabound.cElements - 4; r = new(p[0].ToInt(), p[1].ToInt(), p[2].ToInt(), p[3].ToInt()); } UiaApi.SafeArrayDestroy(sap); return n > 0; } } public static bool GetCaretRectInPowerShell(out RECT r) { //GetGUIThreadInfo and MSAA don't work with PowerShell. // Does not support IUIAutomationTextPattern2. // IUIAutomationTextPattern.GetSelection -> IUIAutomationTextRange.GetBoundingRectangles returns client coord, which may be fixed in the future. // Win+; doesn't work too. But PhraseExpress works. And IME (interesting: temporarily replaces the caret). // I would't care, but this was a user request. //Now instead using an undocumented PS feature. var t = ElementFocused(); if (t != null && 0 == t.get_CurrentControlType(out var ct) && ct == UiaApi.TypeId.Edit) { if (0 == Uia.get_RawViewWalker(out var walker)) { UiaApi.IUIAutomationElement e = null; while (0 == (e == null ? walker.GetFirstChildElement(t, out e) : walker.GetNextSiblingElement(e, out e)) && e != null) { //if (0 == e.get_CurrentControlType(out ct)) print.it(ct); //if (0 == e.get_CurrentAutomationId(out var ai)) print.it(ai); if (0 == e.get_CurrentAutomationId(out var ai) && ai.Find("Caret", true) >= 0 && 0 == e.get_CurrentControlType(out ct) && ct == UiaApi.TypeId.Custom) { if (0 == e.get_CurrentBoundingRectangle(out r)) { return true; } } } //There are 3 child elements. The first is caret. //This is slow. Tested MSAA, but it gets only child element "selection", and cannot get rect when selection empty. } } r = default; return false; } ///// ///// Gets text of TextPattern paragraph from point. ///// ///// ///// Screen coordinates. ///// null if the element does not support TextPattern or if failed. //public static string PatternTextFromPoint(this UiaApi.IUIAutomationElement t, POINT xy) { // if (0 == t.GetCurrentPattern(UiaApi.UIA_TextPatternId, out var o) && o is UiaApi.IUIAutomationTextPattern p) { // if (0 == p.RangeFromPoint(xy, out var tr) && 0 == tr.ExpandToEnclosingUnit(UiaApi.TextUnit.TextUnit_Paragraph)) { // if (0 == tr.GetText(5000, out var s)) return s; // } // } // return null; //} ///// ///// Gets text of ValuePattern. ///// ///// ///// null if the element does not support ValuePattern or if failed. //public static string ValueText(this UiaApi.IUIAutomationElement t) { // if (0 == t.GetCurrentPattern(UiaApi.UIA_ValuePatternId, out var o) && o is UiaApi.IUIAutomationValuePattern p) { // if (0 == p.get_CurrentValue(out var s)) return s; // } // return null; //} } #pragma warning disable 1591, 649, 169 /// /// Declarations of some UI Automation API. /// unsafe class UiaApi : NativeApi { [ComImport, Guid("ff48dba4-60ef-4201-aa87-54103eef594e"), ClassInterface(ClassInterfaceType.None)] internal class CUIAutomation { } [ComImport, Guid("e22ad333-b25f-460c-83d0-0581107395c9"), ClassInterface(ClassInterfaceType.None)] internal class CUIAutomation8 { } [ComImport, Guid("30cbe57d-d9d0-452a-ab13-7ac5ac4825ee"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IUIAutomation { [PreserveSig] int CompareElements(IUIAutomationElement el1, IUIAutomationElement el2, [MarshalAs(UnmanagedType.Bool)] out bool areSame); [PreserveSig] int CompareRuntimeIds(SAFEARRAY* runtimeId1, SAFEARRAY* runtimeId2, [MarshalAs(UnmanagedType.Bool)] out bool areSame); [PreserveSig] int GetRootElement(out IUIAutomationElement root); [PreserveSig] int ElementFromHandle(void* hwnd, out IUIAutomationElement element); [PreserveSig] int ElementFromPoint(POINT pt, out IUIAutomationElement element); [PreserveSig] int GetFocusedElement(out IUIAutomationElement element); [PreserveSig] int GetRootElementBuildCache(IUIAutomationCacheRequest cacheRequest, out IUIAutomationElement root); [PreserveSig] int ElementFromHandleBuildCache(void* hwnd, IUIAutomationCacheRequest cacheRequest, out IUIAutomationElement element); [PreserveSig] int ElementFromPointBuildCache(POINT pt, IUIAutomationCacheRequest cacheRequest, out IUIAutomationElement element); [PreserveSig] int GetFocusedElementBuildCache(IUIAutomationCacheRequest cacheRequest, out IUIAutomationElement element); [PreserveSig] int CreateTreeWalker(IUIAutomationCondition pCondition, out IUIAutomationTreeWalker walker); [PreserveSig] int get_ControlViewWalker(out IUIAutomationTreeWalker walker); [PreserveSig] int get_ContentViewWalker(out IUIAutomationTreeWalker walker); [PreserveSig] int get_RawViewWalker(out IUIAutomationTreeWalker walker); [PreserveSig] int get_RawViewCondition(out IUIAutomationCondition condition); [PreserveSig] int get_ControlViewCondition(out IUIAutomationCondition condition); [PreserveSig] int get_ContentViewCondition(out IUIAutomationCondition condition); [PreserveSig] int CreateCacheRequest(out IUIAutomationCacheRequest cacheRequest); [PreserveSig] int CreateTrueCondition(out IUIAutomationCondition newCondition); [PreserveSig] int CreateFalseCondition(out IUIAutomationCondition newCondition); [PreserveSig] int CreatePropertyCondition(int propertyId, object value, out IUIAutomationCondition newCondition); [PreserveSig] int CreatePropertyConditionEx(int propertyId, object value, PropertyConditionFlags flags, out IUIAutomationCondition newCondition); [PreserveSig] int CreateAndCondition(IUIAutomationCondition condition1, IUIAutomationCondition condition2, out IUIAutomationCondition newCondition); [PreserveSig] int CreateAndConditionFromArray(SAFEARRAY* conditions, out IUIAutomationCondition newCondition); [PreserveSig] int CreateAndConditionFromNativeArray([MarshalAs(UnmanagedType.LPArray)][In] IUIAutomationCondition[] conditions, int conditionCount, out IUIAutomationCondition newCondition); [PreserveSig] int CreateOrCondition(IUIAutomationCondition condition1, IUIAutomationCondition condition2, out IUIAutomationCondition newCondition); [PreserveSig] int CreateOrConditionFromArray(SAFEARRAY* conditions, out IUIAutomationCondition newCondition); [PreserveSig] int CreateOrConditionFromNativeArray([MarshalAs(UnmanagedType.LPArray)][In] IUIAutomationCondition[] conditions, int conditionCount, out IUIAutomationCondition newCondition); [PreserveSig] int CreateNotCondition(IUIAutomationCondition condition, out IUIAutomationCondition newCondition); [PreserveSig] int AddAutomationEventHandler(int eventId, IUIAutomationElement element, TreeScope scope, IUIAutomationCacheRequest cacheRequest, IUIAutomationEventHandler handler); [PreserveSig] int RemoveAutomationEventHandler(int eventId, IUIAutomationElement element, IUIAutomationEventHandler handler); [PreserveSig] int AddPropertyChangedEventHandlerNativeArray(IUIAutomationElement element, TreeScope scope, IUIAutomationCacheRequest cacheRequest, IUIAutomationPropertyChangedEventHandler handler, [MarshalAs(UnmanagedType.LPArray)][In] int[] propertyArray, int propertyCount); [PreserveSig] int AddPropertyChangedEventHandler(IUIAutomationElement element, TreeScope scope, IUIAutomationCacheRequest cacheRequest, IUIAutomationPropertyChangedEventHandler handler, SAFEARRAY* propertyArray); [PreserveSig] int RemovePropertyChangedEventHandler(IUIAutomationElement element, IUIAutomationPropertyChangedEventHandler handler); [PreserveSig] int AddStructureChangedEventHandler(IUIAutomationElement element, TreeScope scope, IUIAutomationCacheRequest cacheRequest, IUIAutomationStructureChangedEventHandler handler); [PreserveSig] int RemoveStructureChangedEventHandler(IUIAutomationElement element, IUIAutomationStructureChangedEventHandler handler); [PreserveSig] int AddFocusChangedEventHandler(IUIAutomationCacheRequest cacheRequest, IUIAutomationFocusChangedEventHandler handler); [PreserveSig] int RemoveFocusChangedEventHandler(IUIAutomationFocusChangedEventHandler handler); [PreserveSig] int RemoveAllEventHandlers(); [PreserveSig] int IntNativeArrayToSafeArray([MarshalAs(UnmanagedType.LPArray)][In] int[] array, int arrayCount, out SAFEARRAY* safeArray); [PreserveSig] int IntSafeArrayToNativeArray(SAFEARRAY* intArray, out int* array, out int arrayCount); [PreserveSig] int RectToVariant(RECT rc, out object var); [PreserveSig] int VariantToRect(object var, out RECT rc); [PreserveSig] int SafeArrayToRectNativeArray(SAFEARRAY* rects, out RECT* rectArray, out int rectArrayCount); [PreserveSig] int CreateProxyFactoryEntry(); [PreserveSig] int get_ProxyFactoryMapping(); [PreserveSig] int GetPropertyProgrammaticName(int property, out string name); [PreserveSig] int GetPatternProgrammaticName(int pattern, out string name); [PreserveSig] int PollForPotentialSupportedPatterns(IUIAutomationElement pElement, out SAFEARRAY* patternIds, out SAFEARRAY* patternNames); [PreserveSig] int PollForPotentialSupportedProperties(IUIAutomationElement pElement, out SAFEARRAY* propertyIds, out SAFEARRAY* propertyNames); [PreserveSig] int CheckNotSupported(object value, [MarshalAs(UnmanagedType.Bool)] out bool isNotSupported); [PreserveSig] int get_ReservedNotSupportedValue([MarshalAs(UnmanagedType.IUnknown)] out object notSupportedValue); [PreserveSig] int get_ReservedMixedAttributeValue([MarshalAs(UnmanagedType.IUnknown)] out object mixedAttributeValue); [PreserveSig] int ElementFromIAccessible(); [PreserveSig] int ElementFromIAccessibleBuildCache(); } [ComImport, Guid("c270f6b5-5c69-4290-9745-7a7f97169468"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IUIAutomationFocusChangedEventHandler { [PreserveSig] int HandleFocusChangedEvent(IUIAutomationElement sender); } [ComImport, Guid("e81d1b4e-11c5-42f8-9754-e7036c79f054"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IUIAutomationStructureChangedEventHandler { [PreserveSig] int HandleStructureChangedEvent(IUIAutomationElement sender, StructureChangeType changeType, SAFEARRAY* runtimeId); } [ComImport, Guid("40cd37d4-c756-4b0c-8c6f-bddfeeb13b50"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IUIAutomationPropertyChangedEventHandler { [PreserveSig] int HandlePropertyChangedEvent(IUIAutomationElement sender, int propertyId, object newValue); } [ComImport, Guid("146c3c17-f12e-4e22-8c27-f894b9b79c69"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IUIAutomationEventHandler { [PreserveSig] int HandleAutomationEvent(IUIAutomationElement sender, int eventId); } [Flags] internal enum PropertyConditionFlags : uint { PropertyConditionFlags_None, PropertyConditionFlags_IgnoreCase, PropertyConditionFlags_MatchSubstring } [ComImport, Guid("4042c624-389c-4afc-a630-9df854a541fc"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IUIAutomationTreeWalker { [PreserveSig] int GetParentElement(IUIAutomationElement element, out IUIAutomationElement parent); [PreserveSig] int GetFirstChildElement(IUIAutomationElement element, out IUIAutomationElement first); [PreserveSig] int GetLastChildElement(IUIAutomationElement element, out IUIAutomationElement last); [PreserveSig] int GetNextSiblingElement(IUIAutomationElement element, out IUIAutomationElement next); [PreserveSig] int GetPreviousSiblingElement(IUIAutomationElement element, out IUIAutomationElement previous); [PreserveSig] int NormalizeElement(IUIAutomationElement element, out IUIAutomationElement normalized); [PreserveSig] int GetParentElementBuildCache(IUIAutomationElement element, IUIAutomationCacheRequest cacheRequest, out IUIAutomationElement parent); [PreserveSig] int GetFirstChildElementBuildCache(IUIAutomationElement element, IUIAutomationCacheRequest cacheRequest, out IUIAutomationElement first); [PreserveSig] int GetLastChildElementBuildCache(IUIAutomationElement element, IUIAutomationCacheRequest cacheRequest, out IUIAutomationElement last); [PreserveSig] int GetNextSiblingElementBuildCache(IUIAutomationElement element, IUIAutomationCacheRequest cacheRequest, out IUIAutomationElement next); [PreserveSig] int GetPreviousSiblingElementBuildCache(IUIAutomationElement element, IUIAutomationCacheRequest cacheRequest, out IUIAutomationElement previous); [PreserveSig] int NormalizeElementBuildCache(IUIAutomationElement element, IUIAutomationCacheRequest cacheRequest, out IUIAutomationElement normalized); [PreserveSig] int get_Condition(out IUIAutomationCondition condition); } [ComImport, Guid("d22108aa-8ac5-49a5-837b-37bbb3d7591e"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IUIAutomationElement { [PreserveSig] int SetFocus(); [PreserveSig] int GetRuntimeId(out SAFEARRAY* runtimeId); [PreserveSig] int FindFirst(TreeScope scope, IUIAutomationCondition condition, out IUIAutomationElement found); [PreserveSig] int FindAll(TreeScope scope, IUIAutomationCondition condition, out IUIAutomationElementArray found); [PreserveSig] int FindFirstBuildCache(TreeScope scope, IUIAutomationCondition condition, IUIAutomationCacheRequest cacheRequest, out IUIAutomationElement found); [PreserveSig] int FindAllBuildCache(TreeScope scope, IUIAutomationCondition condition, IUIAutomationCacheRequest cacheRequest, out IUIAutomationElementArray found); [PreserveSig] int BuildUpdatedCache(IUIAutomationCacheRequest cacheRequest, out IUIAutomationElement updatedElement); [PreserveSig] int GetCurrentPropertyValue(int propertyId, out object retVal); [PreserveSig] int GetCurrentPropertyValueEx(int propertyId, [MarshalAs(UnmanagedType.Bool)] bool ignoreDefaultValue, out object retVal); [PreserveSig] int GetCachedPropertyValue(int propertyId, out object retVal); [PreserveSig] int GetCachedPropertyValueEx(int propertyId, [MarshalAs(UnmanagedType.Bool)] bool ignoreDefaultValue, out object retVal); [PreserveSig] int GetCurrentPatternAs(int patternId, in Guid riid, void** patternObject); [PreserveSig] int GetCachedPatternAs(int patternId, in Guid riid, void** patternObject); [PreserveSig] int GetCurrentPattern(int patternId, [MarshalAs(UnmanagedType.IUnknown)] out object patternObject); [PreserveSig] int GetCachedPattern(int patternId, [MarshalAs(UnmanagedType.IUnknown)] out object patternObject); [PreserveSig] int GetCachedParent(out IUIAutomationElement parent); [PreserveSig] int GetCachedChildren(out IUIAutomationElementArray children); [PreserveSig] int get_CurrentProcessId(out int retVal); [PreserveSig] int get_CurrentControlType(out TypeId retVal); [PreserveSig] int get_CurrentLocalizedControlType(out string retVal); [PreserveSig] int get_CurrentName(out string retVal); [PreserveSig] int get_CurrentAcceleratorKey(out string retVal); [PreserveSig] int get_CurrentAccessKey(out string retVal); [PreserveSig] int get_CurrentHasKeyboardFocus([MarshalAs(UnmanagedType.Bool)] out bool retVal); [PreserveSig] int get_CurrentIsKeyboardFocusable([MarshalAs(UnmanagedType.Bool)] out bool retVal); [PreserveSig] int get_CurrentIsEnabled([MarshalAs(UnmanagedType.Bool)] out bool retVal); [PreserveSig] int get_CurrentAutomationId(out string retVal); [PreserveSig] int get_CurrentClassName(out string retVal); [PreserveSig] int get_CurrentHelpText(out string retVal); [PreserveSig] int get_CurrentCulture(out int retVal); [PreserveSig] int get_CurrentIsControlElement([MarshalAs(UnmanagedType.Bool)] out bool retVal); [PreserveSig] int get_CurrentIsContentElement([MarshalAs(UnmanagedType.Bool)] out bool retVal); [PreserveSig] int get_CurrentIsPassword([MarshalAs(UnmanagedType.Bool)] out bool retVal); [PreserveSig] int get_CurrentNativeWindowHandle(out wnd retVal); [PreserveSig] int get_CurrentItemType(out string retVal); [PreserveSig] int get_CurrentIsOffscreen([MarshalAs(UnmanagedType.Bool)] out bool retVal); [PreserveSig] int get_CurrentOrientation(out OrientationType retVal); [PreserveSig] int get_CurrentFrameworkId(out string retVal); [PreserveSig] int get_CurrentIsRequiredForForm([MarshalAs(UnmanagedType.Bool)] out bool retVal); [PreserveSig] int get_CurrentItemStatus(out string retVal); [PreserveSig] int get_CurrentBoundingRectangle(out RECT retVal); [PreserveSig] int get_CurrentLabeledBy(out IUIAutomationElement retVal); [PreserveSig] int get_CurrentAriaRole(out string retVal); [PreserveSig] int get_CurrentAriaProperties(out string retVal); [PreserveSig] int get_CurrentIsDataValidForForm([MarshalAs(UnmanagedType.Bool)] out bool retVal); [PreserveSig] int get_CurrentControllerFor(out IUIAutomationElementArray retVal); [PreserveSig] int get_CurrentDescribedBy(out IUIAutomationElementArray retVal); [PreserveSig] int get_CurrentFlowsTo(out IUIAutomationElementArray retVal); [PreserveSig] int get_CurrentProviderDescription(out string retVal); [PreserveSig] int get_CachedProcessId(out int retVal); [PreserveSig] int get_CachedControlType(out int retVal); [PreserveSig] int get_CachedLocalizedControlType(out string retVal); [PreserveSig] int get_CachedName(out string retVal); [PreserveSig] int get_CachedAcceleratorKey(out string retVal); [PreserveSig] int get_CachedAccessKey(out string retVal); [PreserveSig] int get_CachedHasKeyboardFocus([MarshalAs(UnmanagedType.Bool)] out bool retVal); [PreserveSig] int get_CachedIsKeyboardFocusable([MarshalAs(UnmanagedType.Bool)] out bool retVal); [PreserveSig] int get_CachedIsEnabled([MarshalAs(UnmanagedType.Bool)] out bool retVal); [PreserveSig] int get_CachedAutomationId(out string retVal); [PreserveSig] int get_CachedClassName(out string retVal); [PreserveSig] int get_CachedHelpText(out string retVal); [PreserveSig] int get_CachedCulture(out int retVal); [PreserveSig] int get_CachedIsControlElement([MarshalAs(UnmanagedType.Bool)] out bool retVal); [PreserveSig] int get_CachedIsContentElement([MarshalAs(UnmanagedType.Bool)] out bool retVal); [PreserveSig] int get_CachedIsPassword([MarshalAs(UnmanagedType.Bool)] out bool retVal); [PreserveSig] int get_CachedNativeWindowHandle(void** retVal); [PreserveSig] int get_CachedItemType(out string retVal); [PreserveSig] int get_CachedIsOffscreen([MarshalAs(UnmanagedType.Bool)] out bool retVal); [PreserveSig] int get_CachedOrientation(out OrientationType retVal); [PreserveSig] int get_CachedFrameworkId(out string retVal); [PreserveSig] int get_CachedIsRequiredForForm([MarshalAs(UnmanagedType.Bool)] out bool retVal); [PreserveSig] int get_CachedItemStatus(out string retVal); [PreserveSig] int get_CachedBoundingRectangle(out RECT retVal); [PreserveSig] int get_CachedLabeledBy(out IUIAutomationElement retVal); [PreserveSig] int get_CachedAriaRole(out string retVal); [PreserveSig] int get_CachedAriaProperties(out string retVal); [PreserveSig] int get_CachedIsDataValidForForm([MarshalAs(UnmanagedType.Bool)] out bool retVal); [PreserveSig] int get_CachedControllerFor(out IUIAutomationElementArray retVal); [PreserveSig] int get_CachedDescribedBy(out IUIAutomationElementArray retVal); [PreserveSig] int get_CachedFlowsTo(out IUIAutomationElementArray retVal); [PreserveSig] int get_CachedProviderDescription(out string retVal); [PreserveSig] int GetClickablePoint(out POINT clickable, [MarshalAs(UnmanagedType.Bool)] out bool gotClickable); } internal enum OrientationType { OrientationType_None, OrientationType_Horizontal, OrientationType_Vertical } [ComImport, Guid("b32a92b5-bc25-4078-9c08-d7ee95c48e03"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IUIAutomationCacheRequest { [PreserveSig] int AddProperty(int propertyId); [PreserveSig] int AddPattern(int patternId); [PreserveSig] int Clone(out IUIAutomationCacheRequest clonedRequest); [PreserveSig] int get_TreeScope(out TreeScope scope); [PreserveSig] int put_TreeScope(TreeScope scope); [PreserveSig] int get_TreeFilter(out IUIAutomationCondition filter); [PreserveSig] int put_TreeFilter(IUIAutomationCondition filter); [PreserveSig] int get_AutomationElementMode(out AutomationElementMode mode); [PreserveSig] int put_AutomationElementMode(AutomationElementMode mode); } [ComImport, Guid("14314595-b4bc-4055-95f2-58f2e42c9855"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IUIAutomationElementArray { [PreserveSig] int get_Length(out int length); [PreserveSig] int GetElement(int index, out IUIAutomationElement element); } [ComImport, Guid("352ffba8-0973-437c-a61f-f64cafd81df9"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IUIAutomationCondition { } [Flags] internal enum TreeScope : uint { TreeScope_None, TreeScope_Element, TreeScope_Children, TreeScope_Descendants = 0x4, TreeScope_Parent = 0x8, TreeScope_Ancestors = 0x10, TreeScope_Subtree = 0x7 } internal struct SAFEARRAY { public ushort cDims; public ushort fFeatures; public uint cbElements; public uint cLocks; public void* pvData; /*[MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]*/ public SAFEARRAYBOUND rgsabound; } internal struct SAFEARRAYBOUND { public uint cElements; public int lLbound; } internal enum AutomationElementMode { AutomationElementMode_None, AutomationElementMode_Full } internal enum StructureChangeType { StructureChangeType_ChildAdded, StructureChangeType_ChildRemoved, StructureChangeType_ChildrenInvalidated, StructureChangeType_ChildrenBulkAdded, StructureChangeType_ChildrenBulkRemoved, StructureChangeType_ChildrenReordered } internal enum TypeId { Button = 50000, Calendar = 50001, CheckBox = 50002, ComboBox = 50003, Edit = 50004, Hyperlink = 50005, Image = 50006, ListItem = 50007, List = 50008, Menu = 50009, MenuBar = 50010, MenuItem = 50011, ProgressBar = 50012, RadioButton = 50013, ScrollBar = 50014, Slider = 50015, Spinner = 50016, StatusBar = 50017, Tab = 50018, TabItem = 50019, Text = 50020, ToolBar = 50021, ToolTip = 50022, Tree = 50023, TreeItem = 50024, Custom = 50025, Group = 50026, Thumb = 50027, DataGrid = 50028, DataItem = 50029, Document = 50030, SplitButton = 50031, Window = 50032, Pane = 50033, Header = 50034, HeaderItem = 50035, Table = 50036, TitleBar = 50037, Separator = 50038, SemanticZoom = 50039, AppBar = 50040, CustomLandmark = 80000, FormLandmark = 80001, MainLandmark = 80002, NavigationLandmark = 80003, SearchLandmark = 80004, } [DllImport("oleaut32.dll", EntryPoint = "#16", PreserveSig = true)] internal static extern int SafeArrayDestroy(SAFEARRAY* psa); public const int UIA_TextPatternId = 10014; internal const int UIA_TextPattern2Id = 10024; [ComImport, Guid("32eba289-3583-42c9-9c59-3b6d9a1e9b6a"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IUIAutomationTextPattern { [PreserveSig] int RangeFromPoint(POINT pt, out IUIAutomationTextRange range); [PreserveSig] int RangeFromChild(IUIAutomationElement child, out IUIAutomationTextRange range); [PreserveSig] int GetSelection(out IUIAutomationTextRangeArray ranges); [PreserveSig] int GetVisibleRanges(out IUIAutomationTextRangeArray ranges); [PreserveSig] int get_DocumentRange(out IUIAutomationTextRange range); [PreserveSig] int get_SupportedTextSelection(out SupportedTextSelection supportedTextSelection); } public enum SupportedTextSelection { SupportedTextSelection_None, SupportedTextSelection_Single, SupportedTextSelection_Multiple } [ComImport, Guid("ce4ae76a-e717-4c98-81ea-47371d028eb6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IUIAutomationTextRangeArray { [PreserveSig] int get_Length(out int length); [PreserveSig] int GetElement(int index, out IUIAutomationTextRange element); } [ComImport, Guid("a543cc6a-f4ae-494b-8239-c814481187a8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IUIAutomationTextRange { [PreserveSig] int Clone(out IUIAutomationTextRange clonedRange); [PreserveSig] int Compare(IUIAutomationTextRange range, [MarshalAs(UnmanagedType.Bool)] out bool areSame); [PreserveSig] int CompareEndpoints(TextPatternRangeEndpoint srcEndPoint, IUIAutomationTextRange range, TextPatternRangeEndpoint targetEndPoint, out int compValue); [PreserveSig] int ExpandToEnclosingUnit(TextUnit textUnit); [PreserveSig] int FindAttribute(int attr, object val, [MarshalAs(UnmanagedType.Bool)] bool backward, out IUIAutomationTextRange found); [PreserveSig] int FindText(string text, [MarshalAs(UnmanagedType.Bool)] bool backward, [MarshalAs(UnmanagedType.Bool)] bool ignoreCase, out IUIAutomationTextRange found); [PreserveSig] int GetAttributeValue(int attr, out object value); [PreserveSig] int GetBoundingRectangles(out SAFEARRAY* boundingRects); [PreserveSig] int GetEnclosingElement(out IUIAutomationElement enclosingElement); [PreserveSig] int GetText(int maxLength, out string text); [PreserveSig] int Move(TextUnit unit, int count, out int moved); [PreserveSig] int MoveEndpointByUnit(TextPatternRangeEndpoint endpoint, TextUnit unit, int count, out int moved); [PreserveSig] int MoveEndpointByRange(TextPatternRangeEndpoint srcEndPoint, IUIAutomationTextRange range, TextPatternRangeEndpoint targetEndPoint); [PreserveSig] int Select(); [PreserveSig] int AddToSelection(); [PreserveSig] int RemoveFromSelection(); [PreserveSig] int ScrollIntoView([MarshalAs(UnmanagedType.Bool)] bool alignToTop); [PreserveSig] int GetChildren(out IUIAutomationElementArray children); } [ComImport, Guid("506a921a-fcc9-409f-b23b-37eb74106872"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IUIAutomationTextPattern2 : IUIAutomationTextPattern { // IUIAutomationTextPattern [PreserveSig] new int RangeFromPoint(POINT pt, out IUIAutomationTextRange range); [PreserveSig] new int RangeFromChild(IUIAutomationElement child, out IUIAutomationTextRange range); [PreserveSig] new int GetSelection(out IUIAutomationTextRangeArray ranges); [PreserveSig] new int GetVisibleRanges(out IUIAutomationTextRangeArray ranges); [PreserveSig] new int get_DocumentRange(out IUIAutomationTextRange range); [PreserveSig] new int get_SupportedTextSelection(out SupportedTextSelection supportedTextSelection); // IUIAutomationTextPattern2 [PreserveSig] int RangeFromAnnotation(IUIAutomationElement annotation, out IUIAutomationTextRange range); [PreserveSig] int GetCaretRange([MarshalAs(UnmanagedType.Bool)] out bool isActive, out IUIAutomationTextRange range); } public enum TextUnit { TextUnit_Character, TextUnit_Format, TextUnit_Word, TextUnit_Line, TextUnit_Paragraph, TextUnit_Page, TextUnit_Document } public enum TextPatternRangeEndpoint { TextPatternRangeEndpoint_Start, TextPatternRangeEndpoint_End } } ================================================ FILE: Au/Api/Api_const.cs ================================================ //Windows API constants common to multiple API functions, such as WM_, WS_, errors. namespace Au.Types; static unsafe partial class Api { #region Errors internal const int S_OK = 0; internal const int S_FALSE = 1; internal const int ERROR_FILE_NOT_FOUND = 2; internal const int ERROR_PATH_NOT_FOUND = 3; internal const int ERROR_ACCESS_DENIED = 5; internal const int ERROR_INVALID_HANDLE = 6; internal const int ERROR_NOT_SAME_DEVICE = 17; internal const int ERROR_NO_MORE_FILES = 18; internal const int ERROR_NOT_READY = 21; internal const int ERROR_SHARING_VIOLATION = 32; internal const int ERROR_LOCK_VIOLATION = 33; internal const int ERROR_HANDLE_EOF = 38; internal const int ERROR_BAD_NETPATH = 53; internal const int ERROR_BAD_NET_NAME = 67; internal const int ERROR_FILE_EXISTS = 80; internal const int ERROR_INVALID_PARAMETER = 87; internal const int ERROR_BROKEN_PIPE = 109; internal const int ERROR_SEM_TIMEOUT = 121; internal const int ERROR_INSUFFICIENT_BUFFER = 122; internal const int ERROR_INVALID_NAME = 123; internal const int ERROR_DIR_NOT_EMPTY = 145; internal const int ERROR_ALREADY_EXISTS = 183; internal const int ERROR_MORE_DATA = 234; internal const int ERROR_DIRECTORY = 267; internal const int ERROR_PIPE_CONNECTED = 535; internal const int ERROR_IO_PENDING = 997; internal const int ERROR_UNABLE_TO_REMOVE_REPLACED = 1175; internal const int ERROR_USER_MAPPED_FILE = 1224; internal const int ERROR_PRIVILEGE_NOT_HELD = 1314; internal const int ERROR_INVALID_WINDOW_HANDLE = 1400; internal const int ERROR_TIMEOUT = 1460; internal const int E_NOTIMPL = unchecked((int)0x80004001); internal const int E_NOINTERFACE = unchecked((int)0x80004002); internal const int E_FAIL = unchecked((int)0x80004005); internal const int E_INVALIDARG = unchecked((int)0x80070057); internal const int E_ACCESSDENIED = unchecked((int)0x80070005); internal const int E_OUTOFMEMORY = unchecked((int)0x8007000E); internal const int DISP_E_MEMBERNOTFOUND = unchecked((int)0x80020003); internal const int REGDB_E_CLASSNOTREG = unchecked((int)0x80040154); internal const int RPC_E_SERVER_CANTMARSHAL_DATA = unchecked((int)0x8001000D); internal const int E_POINTER = unchecked((int)0x80004003); #endregion #region WM_ internal const int WM_NULL = 0; internal const int WM_CREATE = 0x0001; internal const int WM_DESTROY = 0x0002; internal const int WM_MOVE = 0x0003; internal const int WM_SIZE = 0x0005; internal const int WM_ACTIVATE = 0x0006; internal const int WM_SETFOCUS = 0x0007; internal const int WM_KILLFOCUS = 0x0008; internal const int WM_ENABLE = 0x000A; internal const int WM_SETREDRAW = 0x000B; internal const int WM_SETTEXT = 0x000C; internal const int WM_GETTEXT = 0x000D; internal const int WM_GETTEXTLENGTH = 0x000E; internal const int WM_PAINT = 0x000F; internal const int WM_CLOSE = 0x0010; internal const int WM_QUERYENDSESSION = 0x0011; internal const int WM_QUERYOPEN = 0x0013; internal const int WM_ENDSESSION = 0x0016; internal const int WM_QUIT = 0x0012; internal const int WM_ERASEBKGND = 0x0014; internal const int WM_SYSCOLORCHANGE = 0x0015; internal const int WM_SHOWWINDOW = 0x0018; internal const int WM_SETTINGCHANGE = 0x001A; internal const int WM_DEVMODECHANGE = 0x001B; internal const int WM_ACTIVATEAPP = 0x001C; internal const int WM_FONTCHANGE = 0x001D; internal const int WM_TIMECHANGE = 0x001E; internal const int WM_CANCELMODE = 0x001F; internal const int WM_SETCURSOR = 0x0020; internal const int WM_MOUSEACTIVATE = 0x0021; internal const int WM_CHILDACTIVATE = 0x0022; internal const int WM_QUEUESYNC = 0x0023; internal const int WM_GETMINMAXINFO = 0x0024; internal const int WM_PAINTICON = 0x0026; internal const int WM_ICONERASEBKGND = 0x0027; internal const int WM_NEXTDLGCTL = 0x0028; internal const int WM_SPOOLERSTATUS = 0x002A; internal const int WM_DRAWITEM = 0x002B; internal const int WM_MEASUREITEM = 0x002C; internal const int WM_DELETEITEM = 0x002D; internal const int WM_VKEYTOITEM = 0x002E; internal const int WM_CHARTOITEM = 0x002F; internal const int WM_SETFONT = 0x0030; internal const int WM_GETFONT = 0x0031; internal const int WM_SETHOTKEY = 0x0032; internal const int WM_GETHOTKEY = 0x0033; internal const int WM_QUERYDRAGICON = 0x0037; internal const int WM_COMPAREITEM = 0x0039; internal const int WM_GETOBJECT = 0x003D; internal const int WM_COMPACTING = 0x0041; internal const int WM_WINDOWPOSCHANGING = 0x0046; internal const int WM_WINDOWPOSCHANGED = 0x0047; internal const int WM_COPYDATA = 0x004A; internal const int WM_CANCELJOURNAL = 0x004B; internal const int WM_NOTIFY = 0x004E; internal const int WM_INPUTLANGCHANGEREQUEST = 0x0050; internal const int WM_INPUTLANGCHANGE = 0x0051; internal const int WM_TCARD = 0x0052; internal const int WM_HELP = 0x0053; internal const int WM_USERCHANGED = 0x0054; internal const int WM_NOTIFYFORMAT = 0x0055; internal const int WM_CONTEXTMENU = 0x007B; internal const int WM_STYLECHANGING = 0x007C; internal const int WM_STYLECHANGED = 0x007D; internal const int WM_DISPLAYCHANGE = 0x007E; internal const int WM_GETICON = 0x007F; internal const int WM_SETICON = 0x0080; internal const int WM_NCCREATE = 0x0081; internal const int WM_NCDESTROY = 0x0082; internal const int WM_NCCALCSIZE = 0x0083; internal const int WM_NCHITTEST = 0x0084; internal const int WM_NCPAINT = 0x0085; internal const int WM_NCACTIVATE = 0x0086; internal const int WM_GETDLGCODE = 0x0087; internal const int WM_SYNCPAINT = 0x0088; internal const int WM_NCMOUSEMOVE = 0x00A0; internal const int WM_NCLBUTTONDOWN = 0x00A1; internal const int WM_NCLBUTTONUP = 0x00A2; internal const int WM_NCLBUTTONDBLCLK = 0x00A3; internal const int WM_NCRBUTTONDOWN = 0x00A4; internal const int WM_NCRBUTTONUP = 0x00A5; internal const int WM_NCRBUTTONDBLCLK = 0x00A6; internal const int WM_NCMBUTTONDOWN = 0x00A7; internal const int WM_NCMBUTTONUP = 0x00A8; internal const int WM_NCMBUTTONDBLCLK = 0x00A9; internal const int WM_NCXBUTTONDOWN = 0x00AB; internal const int WM_NCXBUTTONUP = 0x00AC; internal const int WM_NCXBUTTONDBLCLK = 0x00AD; internal const int WM_INPUT_DEVICE_CHANGE = 0x00FE; internal const int WM_INPUT = 0x00FF; internal const int WM_KEYDOWN = 0x0100; internal const int WM_KEYUP = 0x0101; internal const int WM_CHAR = 0x0102; internal const int WM_DEADCHAR = 0x0103; internal const int WM_SYSKEYDOWN = 0x0104; internal const int WM_SYSKEYUP = 0x0105; internal const int WM_SYSCHAR = 0x0106; internal const int WM_SYSDEADCHAR = 0x0107; internal const int WM_UNICHAR = 0x0109; internal const int WM_IME_STARTCOMPOSITION = 0x010D; internal const int WM_IME_ENDCOMPOSITION = 0x010E; internal const int WM_IME_COMPOSITION = 0x010F; internal const int WM_INITDIALOG = 0x0110; internal const int WM_COMMAND = 0x0111; internal const int WM_SYSCOMMAND = 0x0112; internal const int WM_TIMER = 0x0113; internal const int WM_HSCROLL = 0x0114; internal const int WM_VSCROLL = 0x0115; internal const int WM_INITMENU = 0x0116; internal const int WM_INITMENUPOPUP = 0x0117; internal const int WM_MENUSELECT = 0x011F; internal const int WM_MENUCHAR = 0x0120; internal const int WM_ENTERIDLE = 0x0121; internal const int WM_MENURBUTTONUP = 0x0122; internal const int WM_MENUDRAG = 0x0123; internal const int WM_MENUGETOBJECT = 0x0124; internal const int WM_UNINITMENUPOPUP = 0x0125; internal const int WM_MENUCOMMAND = 0x0126; internal const int WM_CHANGEUISTATE = 0x0127; internal const int WM_UPDATEUISTATE = 0x0128; internal const int WM_QUERYUISTATE = 0x0129; internal const int WM_CTLCOLORMSGBOX = 0x0132; internal const int WM_CTLCOLOREDIT = 0x0133; internal const int WM_CTLCOLORLISTBOX = 0x0134; internal const int WM_CTLCOLORBTN = 0x0135; internal const int WM_CTLCOLORDLG = 0x0136; internal const int WM_CTLCOLORSCROLLBAR = 0x0137; internal const int WM_CTLCOLORSTATIC = 0x0138; internal const int WM_MOUSEFIRST = 0x0200; internal const int WM_MOUSEMOVE = 0x0200; internal const int WM_LBUTTONDOWN = 0x0201; internal const int WM_LBUTTONUP = 0x0202; internal const int WM_LBUTTONDBLCLK = 0x0203; internal const int WM_RBUTTONDOWN = 0x0204; internal const int WM_RBUTTONUP = 0x0205; internal const int WM_RBUTTONDBLCLK = 0x0206; internal const int WM_MBUTTONDOWN = 0x0207; internal const int WM_MBUTTONUP = 0x0208; internal const int WM_MBUTTONDBLCLK = 0x0209; internal const int WM_MOUSEWHEEL = 0x020A; internal const int WM_XBUTTONDOWN = 0x020B; internal const int WM_XBUTTONUP = 0x020C; internal const int WM_XBUTTONDBLCLK = 0x020D; internal const int WM_MOUSEHWHEEL = 0x020E; internal const int WM_MOUSELAST = 0x020E; internal const int WM_PARENTNOTIFY = 0x0210; internal const int WM_ENTERMENULOOP = 0x0211; internal const int WM_EXITMENULOOP = 0x0212; internal const int WM_NEXTMENU = 0x0213; internal const int WM_SIZING = 0x0214; internal const int WM_CAPTURECHANGED = 0x0215; internal const int WM_MOVING = 0x0216; internal const int WM_POWERBROADCAST = 0x0218; internal const int WM_DEVICECHANGE = 0x0219; internal const int WM_MDICREATE = 0x0220; internal const int WM_MDIDESTROY = 0x0221; internal const int WM_MDIACTIVATE = 0x0222; internal const int WM_MDIRESTORE = 0x0223; internal const int WM_MDINEXT = 0x0224; internal const int WM_MDIMAXIMIZE = 0x0225; internal const int WM_MDITILE = 0x0226; internal const int WM_MDICASCADE = 0x0227; internal const int WM_MDIICONARRANGE = 0x0228; internal const int WM_MDIGETACTIVE = 0x0229; internal const int WM_MDISETMENU = 0x0230; internal const int WM_ENTERSIZEMOVE = 0x0231; internal const int WM_EXITSIZEMOVE = 0x0232; internal const int WM_DROPFILES = 0x0233; internal const int WM_MDIREFRESHMENU = 0x0234; internal const int WM_IME_SETCONTEXT = 0x0281; internal const int WM_IME_NOTIFY = 0x0282; internal const int WM_IME_CONTROL = 0x0283; internal const int WM_IME_COMPOSITIONFULL = 0x0284; internal const int WM_IME_SELECT = 0x0285; internal const int WM_IME_CHAR = 0x0286; internal const int WM_IME_REQUEST = 0x0288; internal const int WM_IME_KEYDOWN = 0x0290; internal const int WM_IME_KEYUP = 0x0291; internal const int WM_MOUSEHOVER = 0x02A1; internal const int WM_MOUSELEAVE = 0x02A3; internal const int WM_NCMOUSEHOVER = 0x02A0; internal const int WM_NCMOUSELEAVE = 0x02A2; internal const int WM_WTSSESSION_CHANGE = 0x02B1; internal const int WM_DPICHANGED = 0x2E0; internal const int WM_DPICHANGED_BEFOREPARENT = 0x2E2; internal const int WM_DPICHANGED_AFTERPARENT = 0x02E3; internal const int WM_GETDPISCALEDSIZE = 0x02E4; internal const int WM_CUT = 0x0300; internal const int WM_COPY = 0x0301; internal const int WM_PASTE = 0x0302; internal const int WM_CLEAR = 0x0303; internal const int WM_UNDO = 0x0304; internal const int WM_RENDERFORMAT = 0x0305; internal const int WM_RENDERALLFORMATS = 0x0306; internal const int WM_DESTROYCLIPBOARD = 0x0307; internal const int WM_DRAWCLIPBOARD = 0x0308; internal const int WM_PAINTCLIPBOARD = 0x0309; internal const int WM_VSCROLLCLIPBOARD = 0x030A; internal const int WM_SIZECLIPBOARD = 0x030B; internal const int WM_ASKCBFORMATNAME = 0x030C; internal const int WM_CHANGECBCHAIN = 0x030D; internal const int WM_HSCROLLCLIPBOARD = 0x030E; internal const int WM_QUERYNEWPALETTE = 0x030F; internal const int WM_PALETTEISCHANGING = 0x0310; internal const int WM_PALETTECHANGED = 0x0311; internal const int WM_HOTKEY = 0x0312; internal const int WM_PRINT = 0x0317; internal const int WM_PRINTCLIENT = 0x0318; internal const int WM_APPCOMMAND = 0x0319; internal const int WM_THEMECHANGED = 0x031A; internal const int WM_CLIPBOARDUPDATE = 0x031D; internal const int WM_DWMCOMPOSITIONCHANGED = 0x031E; internal const int WM_DWMNCRENDERINGCHANGED = 0x031F; internal const int WM_DWMCOLORIZATIONCOLORCHANGED = 0x0320; internal const int WM_DWMWINDOWMAXIMIZEDCHANGE = 0x0321; internal const int WM_GETTITLEBARINFOEX = 0x033F; internal const int WM_APP = 0x8000; internal const int WM_USER = 0x0400; internal const int WM_CPL_LAUNCH = WM_USER + 0x1000; internal const int WM_CPL_LAUNCHED = WM_USER + 0x1001; internal const int WM_SYSTIMER = 0x118; internal const int WM_HSHELL_ACCESSIBILITYSTATE = 11; internal const int WM_HSHELL_ACTIVATESHELLWINDOW = 3; internal const int WM_HSHELL_APPCOMMAND = 12; internal const int WM_HSHELL_GETMINRECT = 5; internal const int WM_HSHELL_LANGUAGE = 8; internal const int WM_HSHELL_REDRAW = 6; internal const int WM_HSHELL_TASKMAN = 7; internal const int WM_HSHELL_WINDOWCREATED = 1; internal const int WM_HSHELL_WINDOWDESTROYED = 2; internal const int WM_HSHELL_WINDOWACTIVATED = 4; internal const int WM_HSHELL_WINDOWREPLACED = 13; internal const int WM_REFLECT = 0x2000; #endregion #region control styles, messages etc //ES_, EM_, EN_ internal const WS ES_MULTILINE = (WS)0x4; internal const WS ES_PASSWORD = (WS)0x20; internal const WS ES_AUTOVSCROLL = (WS)0x40; internal const WS ES_AUTOHSCROLL = (WS)0x80; internal const WS ES_WANTRETURN = (WS)0x1000; internal const WS ES_NUMBER = (WS)0x2000; internal const int EM_SETSEL = 0xB1; internal const int EM_SETCUEBANNER = 0x1501; //CBS_, CB_, CBN_ internal const WS CBS_SIMPLE = (WS)1; internal const WS CBS_DROPDOWN = (WS)2; internal const WS CBS_DROPDOWNLIST = (WS)3; internal const WS CBS_AUTOHSCROLL = (WS)0x40; internal const int CB_INSERTSTRING = 330; internal const int CB_SETCUEBANNER = 0x1703; internal const int MN_GETHMENU = 0x1E1; #endregion #region CS_ internal const uint CS_VREDRAW = 0x1; internal const uint CS_HREDRAW = 0x2; internal const uint CS_DBLCLKS = 0x8; internal const uint CS_OWNDC = 0x20; internal const uint CS_CLASSDC = 0x40; internal const uint CS_PARENTDC = 0x80; internal const uint CS_NOCLOSE = 0x200; internal const uint CS_SAVEBITS = 0x800; internal const uint CS_BYTEALIGNCLIENT = 0x1000; internal const uint CS_BYTEALIGNWINDOW = 0x2000; internal const uint CS_GLOBALCLASS = 0x4000; internal const uint CS_IME = 0x10000; internal const uint CS_DROPSHADOW = 0x20000; #endregion #region HT (hit-test) internal const int HTERROR = -2; internal const int HTTRANSPARENT = -1; internal const int HTNOWHERE = 0; internal const int HTCLIENT = 1; internal const int HTCAPTION = 2; internal const int HTSYSMENU = 3; internal const int HTSIZE = 4; internal const int HTMENU = 5; internal const int HTHSCROLL = 6; internal const int HTVSCROLL = 7; internal const int HTMINBUTTON = 8; internal const int HTMAXBUTTON = 9; internal const int HTLEFT = 10; internal const int HTRIGHT = 11; internal const int HTTOP = 12; internal const int HTTOPLEFT = 13; internal const int HTTOPRIGHT = 14; internal const int HTBOTTOM = 15; internal const int HTBOTTOMLEFT = 16; internal const int HTBOTTOMRIGHT = 17; internal const int HTBORDER = 18; internal const int HTOBJECT = 19; internal const int HTCLOSE = 20; internal const int HTHELP = 21; internal const int HTSIZEFIRST = HTLEFT; internal const int HTSIZELAST = HTBOTTOMRIGHT; #endregion #region SC_ internal const int SC_SIZE = 0xF000; internal const int SC_MOVE = 0xF010; internal const int SC_MINIMIZE = 0xF020; internal const int SC_MAXIMIZE = 0xF030; internal const int SC_NEXTWINDOW = 0xF040; internal const int SC_PREVWINDOW = 0xF050; internal const int SC_CLOSE = 0xF060; internal const int SC_VSCROLL = 0xF070; internal const int SC_HSCROLL = 0xF080; internal const int SC_MOUSEMENU = 0xF090; internal const int SC_KEYMENU = 0xF100; internal const int SC_ARRANGE = 0xF110; internal const int SC_RESTORE = 0xF120; internal const int SC_TASKLIST = 0xF130; internal const int SC_SCREENSAVE = 0xF140; internal const int SC_HOTKEY = 0xF150; internal const int SC_DEFAULT = 0xF160; internal const int SC_MONITORPOWER = 0xF170; internal const int SC_CONTEXTHELP = 0xF180; internal const int SC_SEPARATOR = 0xF00F; #endregion #region COLOR_ internal const int COLOR_SCROLLBAR = 0; internal const int COLOR_BACKGROUND = 1; internal const int COLOR_ACTIVECAPTION = 2; internal const int COLOR_INACTIVECAPTION = 3; internal const int COLOR_MENU = 4; internal const int COLOR_WINDOW = 5; internal const int COLOR_WINDOWFRAME = 6; internal const int COLOR_MENUTEXT = 7; internal const int COLOR_WINDOWTEXT = 8; internal const int COLOR_CAPTIONTEXT = 9; internal const int COLOR_ACTIVEBORDER = 10; internal const int COLOR_INACTIVEBORDER = 11; internal const int COLOR_APPWORKSPACE = 12; internal const int COLOR_HIGHLIGHT = 13; internal const int COLOR_HIGHLIGHTTEXT = 14; internal const int COLOR_BTNFACE = 15; internal const int COLOR_BTNSHADOW = 16; internal const int COLOR_GRAYTEXT = 17; internal const int COLOR_BTNTEXT = 18; internal const int COLOR_INACTIVECAPTIONTEXT = 19; internal const int COLOR_BTNHIGHLIGHT = 20; internal const int COLOR_3DDKSHADOW = 21; internal const int COLOR_3DLIGHT = 22; internal const int COLOR_INFOTEXT = 23; internal const int COLOR_INFOBK = 24; internal const int COLOR_HOTLIGHT = 26; internal const int COLOR_GRADIENTACTIVECAPTION = 27; internal const int COLOR_GRADIENTINACTIVECAPTION = 28; internal const int COLOR_MENUHILIGHT = 29; internal const int COLOR_MENUBAR = 30; internal const int COLOR_DESKTOP = 1; internal const int COLOR_3DFACE = 15; internal const int COLOR_3DSHADOW = 16; internal const int COLOR_3DHIGHLIGHT = 20; internal const int COLOR_3DHILIGHT = 20; internal const int COLOR_BTNHILIGHT = 20; #endregion #region QS_ internal const uint QS_KEY = 0x1; internal const uint QS_MOUSEMOVE = 0x2; internal const uint QS_MOUSEBUTTON = 0x4; internal const uint QS_POSTMESSAGE = 0x8; internal const uint QS_TIMER = 0x10; internal const uint QS_PAINT = 0x20; internal const uint QS_SENDMESSAGE = 0x40; internal const uint QS_HOTKEY = 0x80; internal const uint QS_ALLPOSTMESSAGE = 0x100; internal const uint QS_RAWINPUT = 0x400; internal const uint QS_TOUCH = 0x800; internal const uint QS_POINTER = 0x1000; internal const uint QS_MOUSE = 0x6; internal const uint QS_INPUT = 0x1C07; internal const uint QS_ALLEVENTS = 0x1CBF; internal const uint QS_ALLINPUT = 0x1CFF; #endregion #region WAIT_ internal const int WAIT_FAILED = -1; internal const int WAIT_OBJECT_0 = 0x0; internal const int WAIT_ABANDONED = 0x80; internal const int WAIT_ABANDONED_0 = 0x80; internal const int WAIT_IO_COMPLETION = 0xC0; internal const int WAIT_TIMEOUT = 0x102; #endregion #region LR_, IMAGE_ internal const int IMAGE_BITMAP = 0; internal const int IMAGE_ICON = 1; internal const int IMAGE_CURSOR = 2; internal const uint LR_MONOCHROME = 0x1; internal const uint LR_COLOR = 0x2; internal const uint LR_COPYRETURNORG = 0x4; internal const uint LR_COPYDELETEORG = 0x8; internal const uint LR_LOADFROMFILE = 0x10; internal const uint LR_LOADTRANSPARENT = 0x20; internal const uint LR_DEFAULTSIZE = 0x40; internal const uint LR_VGACOLOR = 0x80; internal const uint LR_LOADMAP3DCOLORS = 0x1000; internal const uint LR_CREATEDIBSECTION = 0x2000; internal const uint LR_COPYFROMRESOURCE = 0x4000; internal const uint LR_SHARED = 0x8000; #endregion #region SFGAO_ internal const uint SFGAO_CANCOPY = 1; internal const uint SFGAO_CANMOVE = 2; internal const uint SFGAO_CANLINK = 4; internal const uint SFGAO_STORAGE = 0x00000008; internal const uint SFGAO_CANRENAME = 0x00000010; internal const uint SFGAO_CANDELETE = 0x00000020; internal const uint SFGAO_HASPROPSHEET = 0x00000040; internal const uint SFGAO_DROPTARGET = 0x00000100; internal const uint SFGAO_CAPABILITYMASK = 0x00000177; internal const uint SFGAO_SYSTEM = 0x00001000; internal const uint SFGAO_ENCRYPTED = 0x00002000; internal const uint SFGAO_ISSLOW = 0x00004000; internal const uint SFGAO_GHOSTED = 0x00008000; internal const uint SFGAO_LINK = 0x00010000; internal const uint SFGAO_SHARE = 0x00020000; internal const uint SFGAO_READONLY = 0x00040000; internal const uint SFGAO_HIDDEN = 0x00080000; internal const uint SFGAO_DISPLAYATTRMASK = 0x000FC000; internal const uint SFGAO_FILESYSANCESTOR = 0x10000000; internal const uint SFGAO_FOLDER = 0x20000000; internal const uint SFGAO_FILESYSTEM = 0x40000000; internal const uint SFGAO_HASSUBFOLDER = 0x80000000; internal const uint SFGAO_CONTENTSMASK = 0x80000000; internal const uint SFGAO_VALIDATE = 0x01000000; internal const uint SFGAO_REMOVABLE = 0x02000000; internal const uint SFGAO_COMPRESSED = 0x04000000; internal const uint SFGAO_BROWSABLE = 0x08000000; internal const uint SFGAO_NONENUMERATED = 0x00100000; internal const uint SFGAO_NEWCONTENT = 0x00200000; internal const uint SFGAO_CANMONIKER = 0x00400000; internal const uint SFGAO_HASSTORAGE = 0x00400000; internal const uint SFGAO_STREAM = 0x00400000; internal const uint SFGAO_STORAGEANCESTOR = 0x00800000; internal const uint SFGAO_STORAGECAPMASK = 0x70C50008; internal const uint SFGAO_PKEYSFGAOMASK = 0x81044000; #endregion #region STGM_ internal const uint STGM_DIRECT = 0x0; internal const uint STGM_TRANSACTED = 0x10000; internal const uint STGM_SIMPLE = 0x8000000; internal const uint STGM_READ = 0x0; internal const uint STGM_WRITE = 0x1; internal const uint STGM_READWRITE = 0x2; internal const uint STGM_SHARE_DENY_NONE = 0x40; internal const uint STGM_SHARE_DENY_READ = 0x30; internal const uint STGM_SHARE_DENY_WRITE = 0x20; internal const uint STGM_SHARE_EXCLUSIVE = 0x10; internal const uint STGM_PRIORITY = 0x40000; internal const uint STGM_DELETEONRELEASE = 0x4000000; internal const uint STGM_NOSCRATCH = 0x100000; internal const uint STGM_CREATE = 0x1000; internal const uint STGM_CONVERT = 0x20000; internal const uint STGM_FAILIFTHERE = 0x0; internal const uint STGM_NOSNAPSHOT = 0x200000; internal const uint STGM_DIRECT_SWMR = 0x400000; #endregion #region MA_ internal const int MA_ACTIVATE = 1; internal const int MA_ACTIVATEANDEAT = 2; internal const int MA_NOACTIVATE = 3; internal const int MA_NOACTIVATEANDEAT = 4; #endregion #region MK_ internal const int MK_LBUTTON = 0x1; internal const int MK_RBUTTON = 0x2; internal const int MK_SHIFT = 0x4; internal const int MK_CONTROL = 0x8; internal const int MK_MBUTTON = 0x10; #endregion #region CF_ internal const int CF_TEXT = 1; internal const int CF_BITMAP = 2; internal const int CF_METAFILEPICT = 3; internal const int CF_SYLK = 4; internal const int CF_DIF = 5; internal const int CF_TIFF = 6; internal const int CF_OEMTEXT = 7; internal const int CF_DIB = 8; internal const int CF_PALETTE = 9; //internal const int CF_PENDATA = 10; //obsolete internal const int CF_RIFF = 11; internal const int CF_WAVE = 12; internal const int CF_UNICODETEXT = 13; internal const int CF_ENHMETAFILE = 14; internal const int CF_HDROP = 15; internal const int CF_LOCALE = 16; internal const int CF_DIBV5 = 17; internal const int CF_MAX = 18; //internal const int CF_OWNERDISPLAY = 0x80; //these are rare and not supported by this library //internal const int CF_DSPTEXT = 0x81; //internal const int CF_DSPBITMAP = 0x82; //internal const int CF_DSPMETAFILEPICT = 0x83; //internal const int CF_DSPENHMETAFILE = 0x8E; //internal const int CF_PRIVATEFIRST = 0x200; //internal const int CF_PRIVATELAST = 0x2FF; //internal const int CF_GDIOBJFIRST = 0x300; //internal const int CF_GDIOBJLAST = 0x3FF; #endregion #region misc internal const int IDI_APPLICATION = 32512; internal const int PBT_APMSUSPEND = 0x4; #endregion #region ENUM [Flags] internal enum VARENUM : ushort { VT_EMPTY, VT_NULL, VT_I2, VT_I4, VT_R4, VT_R8, VT_CY, VT_DATE, VT_BSTR, VT_DISPATCH, VT_ERROR, VT_BOOL, VT_VARIANT, VT_UNKNOWN, VT_DECIMAL, VT_I1 = 16, VT_UI1, VT_UI2, VT_UI4, VT_I8, VT_UI8, VT_INT, VT_UINT, VT_VOID, VT_HRESULT, VT_PTR, VT_SAFEARRAY, VT_CARRAY, VT_USERDEFINED, VT_LPSTR, VT_LPWSTR, VT_RECORD = 36, VT_INT_PTR, VT_UINT_PTR, VT_FILETIME = 64, VT_BLOB, VT_STREAM, VT_STORAGE, VT_STREAMED_OBJECT, VT_STORED_OBJECT, VT_BLOB_OBJECT, VT_CF, VT_CLSID, VT_VERSIONED_STREAM, VT_BSTR_BLOB = 0xFFF, VT_VECTOR, VT_ARRAY = 0x2000, VT_BYREF = 0x4000, VT_RESERVED = 0x8000, VT_ILLEGAL = 0xFFFF, VT_ILLEGALMASKED = 0xFFF, VT_TYPEMASK = 0xFFF } #endregion #region strings internal const string string_IES = "Internet Explorer_Server"; #endregion } ================================================ FILE: Au/Api/Api_public.cs ================================================ #pragma warning disable 649, 169 //field never assigned/used #pragma warning disable 1591 //missing XML documentation namespace Au.Types; /// /// Window styles. /// /// /// Reference: Window Styles. /// Here names are without prefix WS_. For example, instead of WS_BORDER use WS.BORDER. Not included constants that are 0 (eg WS_TILED) or are duplicate (eg WS_SIZEBOX is same as WS_THICKFRAME) or consist of multiple other constants (eg WS_TILEDWINDOW). /// [Flags] public enum WS : uint { POPUP = 0x80000000, CHILD = 0x40000000, MINIMIZE = 0x20000000, VISIBLE = 0x10000000, DISABLED = 0x08000000, CLIPSIBLINGS = 0x04000000, CLIPCHILDREN = 0x02000000, MAXIMIZE = 0x01000000, BORDER = 0x00800000, DLGFRAME = 0x00400000, VSCROLL = 0x00200000, HSCROLL = 0x00100000, SYSMENU = 0x00080000, THICKFRAME = 0x00040000, MINIMIZEBOX = 0x00020000, GROUP = 0x00020000, MAXIMIZEBOX = 0x00010000, TABSTOP = 0x00010000, CAPTION = BORDER | DLGFRAME, //these can cause bugs and confusion because consist of several styles. Not useful in C#, because used only to create native windows. //OVERLAPPEDWINDOW = CAPTION | SYSMENU | THICKFRAME | MINIMIZEBOX | MAXIMIZEBOX, //POPUPWINDOW = POPUP | BORDER | SYSMENU, } /// /// Window extended styles. /// /// /// Reference: Extended Window Styles. /// Here names are without prefix WS_EX_. For example, instead of WS_EX_TOOLWINDOW use WSE.TOOLWINDOW. Not included constants that are 0 (eg WS_EX_LEFT). /// [Flags] public enum WSE : uint { DLGMODALFRAME = 0x00000001, NOPARENTNOTIFY = 0x00000004, TOPMOST = 0x00000008, ACCEPTFILES = 0x00000010, TRANSPARENT = 0x00000020, MDICHILD = 0x00000040, TOOLWINDOW = 0x00000080, WINDOWEDGE = 0x00000100, CLIENTEDGE = 0x00000200, CONTEXTHELP = 0x00000400, //LEFT = 0x00000000, RIGHT = 0x00001000, //LTRREADING = 0x00000000, RTLREADING = 0x00002000, //RIGHTSCROLLBAR = 0x00000000, LEFTSCROLLBAR = 0x00004000, CONTROLPARENT = 0x00010000, STATICEDGE = 0x00020000, APPWINDOW = 0x00040000, LAYERED = 0x00080000, NOINHERITLAYOUT = 0x00100000, NOREDIRECTIONBITMAP = 0x00200000, LAYOUTRTL = 0x00400000, COMPOSITED = 0x02000000, NOACTIVATE = 0x08000000, } /// API MSG public struct MSG //WinMSG { public wnd hwnd; public int message; public nint wParam; public nint lParam; public int time; public POINT pt; public override string ToString() { WndUtil.PrintMsg(out string s, this, new() { Indent = false, Number = false }); return s; } public static implicit operator MSG(in System.Windows.Forms.Message m) => new MSG { hwnd = (wnd)m.HWnd, message = m.Msg, wParam = m.WParam, lParam = m.LParam }; public static implicit operator MSG(in System.Windows.Interop.MSG m) => new MSG { hwnd = (wnd)m.hwnd, message = m.message, wParam = m.wParam, lParam = m.lParam, time = m.time, pt = (m.pt_x, m.pt_y) }; } /// flags. [Flags] public enum GTIFlags { CARETBLINKING = 0x1, INMOVESIZE = 0x2, INMENUMODE = 0x4, SYSTEMMENUMODE = 0x8, POPUPMENUMODE = 0x10, } /// API GUITHREADINFO public struct GUITHREADINFO { public int cbSize; public GTIFlags flags; public wnd hwndActive; public wnd hwndFocus; public wnd hwndCapture; public wnd hwndMenuOwner; public wnd hwndMoveSize; public wnd hwndCaret; public RECT rcCaret; } /// API CREATESTRUCT public unsafe struct CREATESTRUCT { public nint lpCreateParams; public IntPtr hInstance; public nint hMenu; public wnd hwndParent; public int cy; public int cx; public int y; public int x; public WS style; public char* lpszName; public char* lpszClass; public WSE dwExStyle; public RStr Name => lpszName == default ? default : new RStr(lpszName, Ptr_.Length(lpszName)); //public string Name => lpszName == default ? null : new string(lpszName); /// /// If lpszClass is atom, returns string with # prefix and atom value, like "#32770". /// public RStr ClassName => (nuint)lpszClass < 0x10000 ? "#" + ((int)lpszClass).ToS() : new RStr(lpszClass, Ptr_.Length(lpszClass)); //public string ClassName => (nuint)lpszClass < 0x10000 ? "#" + ((int)lpszClass).ToS() : new string(lpszClass); //tested and documented: CBT hook can change only x y cx cy. } /// API SIGDN public enum SIGDN : uint { NORMALDISPLAY, PARENTRELATIVEPARSING = 0x80018001, DESKTOPABSOLUTEPARSING = 0x80028000, PARENTRELATIVEEDITING = 0x80031001, DESKTOPABSOLUTEEDITING = 0x8004C000, FILESYSPATH = 0x80058000, URL = 0x80068000, PARENTRELATIVEFORADDRESSBAR = 0x8007C001, PARENTRELATIVE = 0x80080001, PARENTRELATIVEFORUI = 0x80094001 } /// API SetWindowPos flags. Can be used with . [Flags] public enum SWPFlags : uint { NOSIZE = 0x1, NOMOVE = 0x2, NOZORDER = 0x4, NOREDRAW = 0x8, NOACTIVATE = 0x10, FRAMECHANGED = 0x20, SHOWWINDOW = 0x40, HIDEWINDOW = 0x80, NOCOPYBITS = 0x100, NOOWNERZORDER = 0x200, NOSENDCHANGING = 0x400, _NOCLIENTSIZE = 0x800, _NOCLIENTMOVE = 0x1000, DEFERERASE = 0x2000, ASYNCWINDOWPOS = 0x4000, _STATECHANGED = 0x8000, //_10000000 = 0x10000000, _KNOWNFLAGS = 0xffff, //the undocumented flags would break ToString() if not defined } /// /// Special window handle values. Can be used with . /// See API SetWindowPos. /// public enum SpecHWND { TOP = 0, BOTTOM = 1, TOPMOST = -1, NOTOPMOST = -2, MESSAGE = -3, BROADCAST = 0xffff, } /// /// Window long constants. Used with and . /// See API GetWindowLong. See also API SetWindowSubclass. /// public static class GWL { public const int WNDPROC = -4; public const int USERDATA = -21; public const int STYLE = -16; public const int ID = -12; public const int HWNDPARENT = -8; public const int HINSTANCE = -6; public const int EXSTYLE = -20; //info: also there are GWLP_, but their values are the same. public static class DWL { public static readonly int MSGRESULT = 0; public static readonly int DLGPROC = IntPtr.Size; public static readonly int USER = IntPtr.Size * 2; } } /// /// Window class long constants. Used with . /// See API WNDCLASSEX, GetClassLong. /// public static class GCL { public const int ATOM = -32; public const int WNDPROC = -24; public const int STYLE = -26; public const int MENUNAME = -8; public const int HMODULE = -16; public const int HICONSM = -34; public const int HICON = -14; public const int HCURSOR = -12; public const int HBRBACKGROUND = -10; public const int CBWNDEXTRA = -18; public const int CBCLSEXTRA = -20; //info: also there are GCLP_, but their values are the same. } /// API WNDPROC public delegate nint WNDPROC(wnd w, int msg, nint wp, nint lp); /// API SendMessageTimeout flags. Used with . [Flags] public enum SMTFlags : uint { BLOCK = 0x0001, ABORTIFHUNG = 0x0002, NOTIMEOUTIFNOTHUNG = 0x0008, ERRORONEXIT = 0x0020, } /// API DrawTextEx format flags. [Flags] public enum TFFlags { CENTER = 0x1, RIGHT = 0x2, VCENTER = 0x4, BOTTOM = 0x8, WORDBREAK = 0x10, SINGLELINE = 0x20, EXPANDTABS = 0x40, TABSTOP = 0x80, NOCLIP = 0x100, EXTERNALLEADING = 0x200, CALCRECT = 0x400, NOPREFIX = 0x800, INTERNAL = 0x1000, EDITCONTROL = 0x2000, PATH_ELLIPSIS = 0x4000, END_ELLIPSIS = 0x8000, MODIFYSTRING = 0x10000, RTLREADING = 0x20000, WORD_ELLIPSIS = 0x40000, NOFULLWIDTHCHARBREAK = 0x80000, HIDEPREFIX = 0x100000, PREFIXONLY = 0x200000 } ================================================ FILE: Au/Api/Cpp.cs ================================================ using static Au.Types.GWL; namespace Au.Types; [DebuggerStepThrough] static unsafe partial class Cpp { static List<(string dll, nint h)> _dlls = []; static Cpp() { LoadAuNativeDll("AuCpp.dll"); Cpp_SetHelperCallback(&_HelperCallback); //note: not in elm ctor. Eg by the elm tool not via elm. Fast. } /// /// Loads correct 64/32/ARM64 version of a private native dll. Then [DllImport] will use it. /// Used for Au dlls (AuCpp) and LA dlls (Scintilla). /// /// Dll file name like "name.dll". /// Handle. /// /// /// Searches in: /// - subfolder 64 or 32 or 64\ARM of the Au.dll folder. /// - calls NativeLibrary.TryLoad, which works like simple [DllImport], eg may use info from deps.json. /// - subfolder 64 etc of folder specified in environment variable Au.Path. For example the dll is unavailable if used in an assembly (managed dll) loaded in a nonstandard environment, eg VS forms designer or VS C# Interactive (then folders.ThisApp is "C:\Program Files (x86)\Microsoft Visual Studio\..."). Workaround: set environment variable Au.Path = the main Au directory and restart Windows. /// public static nint LoadAuNativeDll(string fileName) { //Debug.Assert(default == Api.GetModuleHandle(fileName)); //no, asserts if cpp dll is injected by acc nint h = 0; string rel = (RuntimeInformation.ProcessArchitecture switch { Architecture.X86 => @"32\", Architecture.Arm64 => @"64\ARM\", _ => @"64\" }) + fileName; //rejected: use standard NuGet "runtimes" folder instead. I did not find info whether it can be used. //Au.dll dir + rel var asm = typeof(Cpp).Assembly; if (asm.Location is [_, ..] s1) { s1 = s1[..(s1.LastIndexOf('\\') + 1)] + rel; if (NativeLibrary.TryLoad(s1, out h)) return h; } //like [DllImport]. It uses NATIVE_DLL_SEARCH_DIRECTORIES, which was built at startup by our AppHost or from deps.json. // Also finds in temp dir when +. if (NativeLibrary.TryLoad(fileName, asm, null, out h)) return h; //environment variable + rel if (Environment.GetEnvironmentVariable("Au.Path") is string s2) if (NativeLibrary.TryLoad(pathname.combine(s2, rel), out h)) return h; throw new DllNotFoundException(fileName + " not found"); } internal struct Cpp_Acc { public IntPtr acc; public int elem; public elm.Misc_ misc; public Cpp_Acc(IntPtr iacc, int elem_) { acc = iacc; elem = elem_; misc = default; } public Cpp_Acc(elm e) { acc = e._iacc; elem = e._elem; misc = e._misc; } public static implicit operator Cpp_Acc(elm e) => new(e); } internal delegate int Cpp_AccFindCallbackT(Cpp_Acc a, RECT* r); internal struct Cpp_AccFindParams { string _role, _name, _prop; int _roleLength, _nameLength, _propLength; public EFFlags flags; public int skip; char _resultProp; //elmFinder.RProp int _flags2; public Cpp_AccFindParams(string role, string name, string prop, EFFlags flags, int skip, char resultProp) { if (role != null) { _role = role; _roleLength = role.Length; } if (name != null) { _name = name; _nameLength = name.Length; } if (prop != null) { _prop = prop; _propLength = prop.Length; } this.flags = flags; this.skip = skip; _resultProp = resultProp; } /// /// Parses role. Enables Chrome acc if need. /// public void RolePrefix(wnd w) { if (_roleLength < 4) return; int i = _role.IndexOf(':'); if (i < 3) return; _flags2 = Cpp_AccRolePrefix(_role, i, w); if (_flags2 != 0) { _roleLength -= ++i; _role = _roleLength > 0 ? _role[i..] : null; } } } [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] static extern int Cpp_AccRolePrefix(string s, int len, wnd w); [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern EError Cpp_AccFind(wnd w, Cpp_Acc* aParent, Cpp_AccFindParams ap, Cpp_AccFindCallbackT also, out Cpp_Acc aResult, [MarshalAs(UnmanagedType.BStr)] out string sResult, bool getRects = false); internal enum EError { NotFound = 0x1001, //UI element not found. With FindAll - no errors. This is actually not an error. InvalidParameter = 0x1002, //invalid parameter, for example wildcard expression (or regular expression in it) WindowClosed = 0x1003, //the specified window handle is invalid or the window was destroyed while injecting } internal static bool IsCppError(int hr) { return hr >= (int)EError.NotFound && hr <= (int)EError.WindowClosed; } /// /// flags: 1 not inproc, 2 get only name. /// [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern int Cpp_AccFromWindow(int flags, wnd w, EObjid objId, out Cpp_Acc aResult, out BSTR sResult); internal delegate EXYFlags Cpp_AccFromPointCallbackT(EXYFlags flags, wnd wFP, wnd wTL); [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern int Cpp_AccFromPoint(POINT p, EXYFlags flags, Cpp_AccFromPointCallbackT callback, out Cpp_Acc aResult); [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern int Cpp_AccGetFocused(wnd w, EFocusedFlags flags, out Cpp_Acc aResult); //These are called from elm class functions like Cpp.Cpp_Func(this, ...); GC.KeepAlive(this);. //We can use 'this' because Cpp_Acc has an implicit conversion from elm operator. //Need GC.KeepAlive(this) everywhere. Else GC can collect the elm (and release _iacc) while in the Cpp func. //Alternatively could make the Cpp parameter 'const Cpp_Acc&', and pass elm directly. But I don't like it. [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern int Cpp_AccNavigate(Cpp_Acc aFrom, string navig, out Cpp_Acc aResult); [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern int Cpp_AccGetStringProp(Cpp_Acc a, char prop, out BSTR sResult); [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern int Cpp_AccWeb(Cpp_Acc a, string what, out BSTR sResult); [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern int Cpp_AccGetRect(Cpp_Acc a, out RECT r); [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern int Cpp_AccGetRole(Cpp_Acc a, out ERole roleInt, out BSTR roleStr); [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern int Cpp_AccGetInt(Cpp_Acc a, char what, out int R); [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern int Cpp_AccAction(Cpp_Acc a, char action = 'a', [MarshalAs(UnmanagedType.BStr)] string param = null); [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern int Cpp_AccSelect(Cpp_Acc a, ESelect flagsSelect); [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern int Cpp_AccGetSelection(Cpp_Acc a, out BSTR sResult); [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern int Cpp_AccGetProps(Cpp_Acc a, string props, out BSTR sResult); /// 1 - wait less. [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern void Cpp_Unload(uint flags); #if DEBUG internal static void DebugUnload() { GC.Collect(); GC.WaitForPendingFinalizers(); Cpp_Unload(0); } // [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] // internal static extern IInterface Cpp_GetInterface(); // [ComImport, Guid("3AB5235E-2768-47A2-909A-B5852A9D1868"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] // internal interface IInterface { // [PreserveSig] int Add(int a, int b); // //int Prop { set; get; } // //[return: MarshalAs(UnmanagedType.Bool)] bool Prop { set; get; } // //bool Prop { [return: MarshalAs(UnmanagedType.Bool)] set; [return: MarshalAs(UnmanagedType.Bool)] get; } // //[property: MarshalAs(UnmanagedType.Bool)] bool Prop { set; get; } // void put_Prop([MarshalAs(UnmanagedType.Bool)] bool r); // [return: MarshalAs(UnmanagedType.Bool)] bool get_Prop(); // //void put_Prop2([MarshalAs(UnmanagedType.LPStr)] string r); // //[return: MarshalAs(UnmanagedType.LPStr)] string get_Prop2(); // //[return: MarshalAs(UnmanagedType.IUnknown)] object Prop2 { get; set; } // //object Prop2 { [return: MarshalAs(UnmanagedType.IUnknown)] get; set; } //#if false // void put_Prop2([MarshalAs(UnmanagedType.IUnknown)] object r); // [return: MarshalAs(UnmanagedType.IUnknown)] object get_Prop2(); //#else // void put_Prop2(IUnknown r); // IUnknown get_Prop2(); // [ComImport, Guid("00000000-0000-0000-c000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] // internal interface IUnknown { } //#endif // } #endif [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] static extern void Cpp_SetHelperCallback(delegate* unmanaged callback); [UnmanagedCallersOnly] static int _HelperCallback(int action, wnd w) { if (action == 1) { //AccEnableChrome asks to detect whether the command line contains --force-renderer-accessibility. If yes, will not try to enable acc. if (process.getCommandLine(w.ProcessId, removeProgram: true) is string s) { if (s.Contains("--force-renderer-accessibility")) return 1; //print.warning("To use UI elements in web pages, start browser with command line --force-renderer-accessibility. Without it the code may fail sometimes or stop working in the future."); } } else if (action == 2) { //AccEnableChrome failed print.warning("To use UI elements in web pages, start browser with command line --force-renderer-accessibility."); } return 0; } // OTHER [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern IntPtr Cpp_ModuleHandle(); [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern char* Cpp_LowercaseTable(); [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern IntPtr Cpp_Clipboard(IntPtr hh); [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern bool Cpp_ShellExec(in Api.SHELLEXECUTEINFO x, out int pid, out int injectError, out int execError); [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern nint Cpp_AccWorkaround(Api.IAccessible a, nint wParam, ref nint obj); [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern void Cpp_UEF(bool on); [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern void Cpp_InactiveWindowWorkaround(bool on); /// 0 failed, 1 x86, 2 x64, 3 arm64 [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern int Cpp_GetProcessArchitecture(int pid); // TEST #if DEBUG [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern void Cpp_Test(); //[DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] //internal static extern void Cpp_TestWildex(string s, string w); //[DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] //internal static extern int Cpp_TestInt(int a); //[DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] //internal static extern int Cpp_TestString(string a, int b, int c); //[ComImport, Guid("3426CF3C-F7C2-4322-A292-463DB8729B54"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] //internal interface ICppTest //{ // [PreserveSig] int TestInt(int a, int b, int c); // [PreserveSig] int TestString([MarshalAs(UnmanagedType.LPWStr)] string a, int b, int c); // [PreserveSig] int TestBSTR(string a, int b, int c); //} //[DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] //internal static extern ICppTest Cpp_Interface(); //[ComImport, Guid("57017F56-E7CA-4A7B-A8F8-2AE36077F50D"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] //internal interface IThreadExitEvent //{ // [PreserveSig] int Unsubscribe(); //} //[DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] //internal static extern IThreadExitEvent Cpp_ThreadExitEvent(IntPtr callback); //[DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] //internal static extern void Cpp_ThreadExitEvent2(IntPtr callback); #endif } ================================================ FILE: Au/Api/WinRT.cs ================================================ //WinRT common API and utils. Others are in files where used. //This library does not use C#/WinRT (Microsoft.Windows.SDK.NET.dll and WinRT.Runtime.dll). //Would be easier, but it has problems: // - Adds 2 files, 22 MB. // - OCR: if once used in a STA thread, then does not work in MTA threads. // - OCR: slower. namespace Au.Types; #pragma warning disable 649, 169 //field never assigned/used static unsafe partial class WinRT { [DllImport("combase.dll", PreserveSig = true)] internal static extern int WindowsCreateString(string s, int length, out IntPtr hstring); [DllImport("combase.dll", PreserveSig = true)] internal static extern int WindowsDeleteString(IntPtr hstring); [DllImport("combase.dll")] internal static extern char* WindowsGetStringRawBuffer(IntPtr hstring, out int length); //probably don't need. Always returns 1 (already initialized) when called with current apartment state. //[DllImport("combase.dll", PreserveSig = true)] //internal static extern int RoInitialize(int initType); [DllImport("combase.dll", PreserveSig = true)] internal static extern int RoGetActivationFactory(IntPtr activatableClassId, in Guid iid, out IntPtr factory); internal static T Create(string progId) where T : struct { using var hs = new _Hstring(progId); HR(RoGetActivationFactory(hs, typeof(T).GUID, out var r)); return Unsafe.As(ref r); } //rejected: use static factories. Now fast. internal static void HR(int hr) { if (hr < 0) throw Marshal.GetExceptionForHR(hr); } internal static T As(IntPtr ip) where T : unmanaged { Debug.Assert(sizeof(T) == sizeof(IntPtr)); return Unsafe.As(ref ip); } /// /// A COM interface pointer. /// internal struct IUnknown : IDisposable { IntPtr _u; IUnknown(IntPtr iunknown) { _u = iunknown; } public void Dispose() { if (_u != default) { Marshal.Release(_u); _u = default; } } public bool IsNull => _u == default; int _QI(Type type, out IntPtr r) { #if NET9_0_OR_GREATER //changed ref -> in return Marshal.QueryInterface(_u, type.GUID, out r); #else var guid = type.GUID; return Marshal.QueryInterface(_u, ref guid, out r); #endif } /// /// Calls QueryInterface. Throws exception if failed. /// public T QI() where T : unmanaged { HR(_QI(typeof(T), out var r)); return As(r); } /// /// Calls QueryInterface. Returns false if failed. /// public bool QI(out T r) where T : unmanaged { bool ok = 0 == _QI(typeof(T), out var v); r = ok ? As(v) : default; return ok; } /// /// Gets COM interface function at index i in vtbl. /// public nint this[int i] => (*(nint**)_u)[i]; public static implicit operator IUnknown(IntPtr p) => new(p); public static implicit operator IntPtr(IUnknown p) => p._u; public override string ToString() => _u.ToString(); /// /// Calls a 0-param function that returns HSTRING. /// /// Interface function index in vtbl. public string GetString(int i) { using var s1 = new _Hstring(_GetPtr(i)); return s1.ToString(); } IntPtr _GetPtr(int i) { HR(((delegate* unmanaged[Stdcall])this[i])(_u, out var r)); return r; } /// /// Calls a 0-param function with a pointer-size return type (COM pointer, etc). /// /// Interface function index in vtbl. public T GetPtr(int i) where T : unmanaged => As(_GetPtr(i)); //when calling: System.BadImageFormatException: Bad element type in SizeOf. //public IntPtr GetPtr(int i, T1 p1) where T1 : unmanaged { // HR(((delegate* unmanaged[Stdcall])this[i])(_u, p1, out var r)); // return r; //} /// /// QI(IClosable).Close() /// public void Close() { using var c = QI(); c.Close(); } } internal interface IComPtr : IDisposable { } internal struct IVectorView : IComPtr where T : unmanaged { IUnknown _u; public IUnknown U => _u; public void Dispose() => _u.Dispose(); IntPtr _GetAt(int i) { HR(((delegate* unmanaged[Stdcall])_u[6])(_u, i, out var r)); return r; } public T this[int i] => As(_GetAt(i)); public int Size { get { HR(((delegate* unmanaged[Stdcall])_u[7])(_u, out int r)); return r; } } public IEnumerable Items(bool disposeItems = true) { for (int i = 0, n = Size; i < n; i++) { var v = _GetAt(i); yield return As(v); if (disposeItems) Marshal.Release(v); } } } #if true internal struct IAsyncOperation : IComPtr { IUnknown _u; public IUnknown U => _u; public void Dispose() => _u.Dispose(); public T Await(bool dispose = true) where T : unmanaged { try { using (var ai = _u.QI()) { for (int i = 4; ; i++) { var status = ai.Status; //print.it(status, i / 4); if (status == AsyncStatus.Completed) break; if (status != AsyncStatus.Started) throw new AuException(); wait.ms(i / 4); } } return _u.GetPtr(8); //GetResults } finally { if (dispose) Dispose(); } } [Guid("00000036-0000-0000-C000-000000000046")] struct IAsyncInfo : IComPtr { IUnknown _u; public IUnknown U => _u; public void Dispose() => _u.Dispose(); public AsyncStatus Status { get { HR(((delegate* unmanaged[Stdcall])_u[7])(_u, out var r)); return r; } } } enum AsyncStatus { Canceled = 2, Completed = 1, Error = 3, Started = 0 } } #else //tried to use Completed callback, unsuccessfully internal struct IAsyncOperation : IComPtr { IUnknown _u; public IUnknown U => _u; public void Dispose() => _u.Dispose(); public T Await(bool dispose = true) where T : unmanaged { try { //static void _Handler(IAsyncOperation ai, AsyncStatus status) { print.it("completed"); } //delegate* p1 = &_Handler; //HR(((delegate* unmanaged[Stdcall])_u[6])(_u, p1)); //put_Completed var del = new AsyncOperationCompletedHandler(static (IAsyncOperation ai, AsyncStatus status) => { print.it("completed"); }); HR(((delegate* unmanaged[Stdcall])_u[6])(_u, del)); //put_Completed dialog.show(""); GC.KeepAlive(del); return _u.GetPtr(8); //GetResults } finally { if (dispose) Dispose(); } } enum AsyncStatus { Canceled = 2, Completed = 1, Error = 3, Started = 0 } delegate void AsyncOperationCompletedHandler(IAsyncOperation ao, AsyncStatus status); } #endif [Guid("30d5a829-7fa4-4026-83bb-d75bae4ea99e")] internal struct IClosable : IComPtr { IUnknown _u; public IUnknown U => _u; public void Dispose() => _u.Dispose(); public void Close() { HR(((delegate* unmanaged[Stdcall])_u[6])(_u)); } } unsafe class _Hstring : IDisposable { IntPtr _h; public _Hstring(IntPtr hstring) { _h = hstring; } public _Hstring(string s) { WindowsCreateString(s, s.Lenn(), out _h); } public void Dispose() { if (_h != default) { WindowsDeleteString(_h); _h = default; } GC.SuppressFinalize(this); } ~_Hstring() { if (_h != default) WindowsDeleteString(_h); } //public static implicit operator _Hstring(string s) => new(s); public static implicit operator IntPtr(_Hstring s) => s._h; public static implicit operator string(_Hstring s) => s.ToString(); public override string ToString() { if (_h == default) return null; char* p = WindowsGetStringRawBuffer(_h, out int len); return new(p, 0, len); } } } ================================================ FILE: Au/Au.More/AppSingleInstance.cs ================================================ namespace Au.More; /// /// Implements "single instance process" feature. /// /// /// { /// print.it("AppSingleInstance.Notified", a); /// b.Window.Activate(); /// }; /// if (!b.ShowDialog()) return; /// ]]> /// /// /// Implements "single instance process" feature. /// /// public static class AppSingleInstance { static Mutex _mutex; static wnd _wNotify; /// /// Detects whether a process of this app is already running. /// /// A unique string to use for mutex name (see ). If prefix @"Global\" used, detects processes in all user sessions. /// /// If not null: ///
• If already running, sends it to that process, which receives it in event. ///
• Else enables event in this process. /// /// Milliseconds to wait until this process can run. No timeout if -1. /// True if already running. /// This function already called. /// Exceptions of . public static bool AlreadyRunning(string mutex, IEnumerable notifyArgs = null, int waitMS = 0) { var m = new Mutex(true, mutex, out bool createdNew); if (null != Interlocked.CompareExchange(ref _mutex, m, null)) { m.Dispose(); throw new InvalidOperationException(); } if (!createdNew && waitMS != 0) { try { createdNew = m.WaitOne(waitMS); } catch (AbandonedMutexException) { createdNew = true; } } if (notifyArgs != null) { if (createdNew) { WndUtil.RegisterWindowClass(mutex, _WndProc); _wNotify = WndUtil.CreateMessageOnlyWindow(mutex, "AppSingleInstance"); WndCopyData.EnableReceivingWM_COPYDATA(); } else { var w = wnd.findFast("AppSingleInstance", mutex, messageOnly: true); if (!w.Is0) WndCopyData.Send(w, 1, string.Join('\0', notifyArgs)); } } return !createdNew; } static nint _WndProc(wnd w, int msg, nint wp, nint lp) { if (msg == Api.WM_COPYDATA) { var x = new WndCopyData(lp); if (x.DataId == 1) { Notified?.Invoke(x.GetString().Split('\0')); } } return Api.DefWindowProc(w, msg, wp, lp); } /// /// When in new process detected that this process is running. /// Receives notifyArgs passed to it. /// /// /// To enable this event, call with non-null notifyArgs. The event handler runs in the same thread. The thread must dispatch Windows messages (for example show a window or dialog, or call ). /// public static event Action Notified; } ================================================ FILE: Au/Au.More/BufferedPaint.cs ================================================ namespace Au.More; /// /// Wraps buffered paint API BeginBufferedPaint etc. /// Must be disposed locally, like in the example. /// /// /// /// public struct BufferedPaint : IDisposable { //rejected. Users often will forget it. Better init automatically. ///// ///// Calls API BufferedPaintInit. ///// //public static void Init() { Api.BufferedPaintInit(); } //fast ///// ///// Calls API BufferedPaintUnInit. ///// //public static void Uninit() { Api.BufferedPaintUnInit(); } [ThreadStatic] static bool s_inited; //never mind: should BufferedPaintUnInit before thread exits. // Not very important. Usually a process has single UI thread. Tested: 10000 threads without BufferedPaintUnInit don't leak much. // To detect thread exit could use eg FlsAlloc(callback)+FlsSetValue, or unmanaged dll thread detach. // But it can be dangerous (too late, eg C# thread variables are already cleared). wnd _w; IntPtr _dcn, _dcb; bool _wmPaint; Api.PAINTSTRUCT _ps; IntPtr _hb; RECT _r; /// /// Gets non-buffered DC with API BeginPaint or GetDC. Then gets buffered DC with API BeginBufferedPaint for entire client area or rectangle r. /// /// /// Use API BeginPaint/EndPaint. If false, uses GetDC/ReleaseDC. /// Part of client area. public unsafe BufferedPaint(wnd w, bool wmPaint, RECT? r = null) { if (!s_inited) s_inited = 0 == Api.BufferedPaintInit(); _w = w; if (_wmPaint = wmPaint) { _dcn = Api.BeginPaint(w, out _ps); } else { _dcn = Api.GetDC(_w); } _r = r ?? _w.ClientRect; Api.BP_PAINTPARAMS pp = new() { cbSize = sizeof(Api.BP_PAINTPARAMS) }; //var ru = wmPaint ? _ps.rcPaint : _r; //the buffer bitmap is smaller when rcPaint smaller, but in most cases don't need to change painting code, although GetViewportOrgEx etc get 0 offsets of the buffer DC. However problem with brush alignment. _hb = Api.BeginBufferedPaint(_dcn, _r, Api.BP_BUFFERFORMAT.BPBF_TOPDOWNDIB, ref pp, out _dcb); //BPBF_COMPATIBLEBITMAP slower //tested: works with 16 and 8 bit colors too Debug_.PrintIf(_hb == default && !_r.NoArea, $"BeginBufferedPaint, {_r}"); if (_hb == default) _dcb = _dcn; } /// /// Calls API EndBufferedPaint and EndPaint or ReleaseDC. /// public void Dispose() { if (_dcn == default) return; if (_hb != default) Api.EndBufferedPaint(_hb, true); if (_wmPaint) Api.EndPaint(_w, _ps); else Api.ReleaseDC(_w, _dcn); _dcn = default; } /// /// Gets window DC. /// public IntPtr NonBufferedDC => _dcn; /// /// Gets the buffered DC. Returns if API BeginBufferedPaint failed. /// public IntPtr DC => _dcb; /// /// Gets client area rectangle or rectangle passed to constructor. /// public RECT Rect => _r; /// /// Gets bounding rectangle of the update region in client area rectangle. /// public RECT UpdateRect { get { if (_wmPaint) return _ps.rcPaint; Api.GetUpdateRect(_w, out var r, false); return r; } } } ================================================ FILE: Au/Au.More/CheckListDialog.cs ================================================ using System.Windows; using System.Windows.Controls; using System.Collections; namespace Au.More; /// /// Dialog with list of check boxes. /// /// /// /// /// public class CheckListDialog { wpfBuilder _b; TextBlock _text; ListBox _lb; List _ac = new(); /// /// Creates , sets some window properties, optionally sets static text. /// /// Text above the list. Can be null. See also . /// Window name. If null, uses . public CheckListDialog(string text = null, string title = null) { _b = new wpfBuilder(title ?? dialog.options.defaultTitle) .WinProperties(resizeMode: ResizeMode.NoResize) .Width(250..600); _b.R.Add(out _text, text).Wrap(TextWrapping.Wrap); if (text == null) _text.Visibility = Visibility.Collapsed; } /// /// Adds a checkbox. /// /// The checkbox. public CheckBox Add(string text, bool check = false, string tooltip = null) { if (_lb == null) _b.R.Add(out _lb).Height(..550); //var content = UseAccessKey ? text : text?.Replace("_", "__"); var content = text?.Replace("_", "__"); CheckBox c = new() { Content = content, Tag = text, IsChecked = check, ToolTip = tooltip }; _ac.Add(c); _lb.Items.Add(c); return c; } /// /// Adds multiple checkboxes. /// /// Array, List, etc containing text strings for checkboxes. /// Whether to check all checkboxes. public void Add(IEnumerable items, bool check = false) { foreach (var v in items) Add(v, check); } ///// ///// Character '_' in checkbox text isn't displayed. Instead the next character toggles the checkbox when pressed with Alt. For literal "_" use "__". ///// //public bool UseAccessKey { get; set; } /// /// Sets formatted text, like . /// /// public void FormatText(wpfBuilder.InterpolatedString text) { wpfBuilder.formatTextOf(_text, text); _text.Visibility = Visibility.Visible; } /// /// Changes text of OK and Cancel buttons. /// public void SetButtons(string ok = "OK", string cancel = "Cancel") { _ok = ok; _cancel = cancel; } string _ok, _cancel; /// /// Gets the that builds the dialog. /// You can use it to add more controls, change window properties, etc; see example. /// /// /// /// public wpfBuilder Builder => _b; /// /// Adds OK/Cancel buttons, shows dialog, sets result properties. /// /// /// Owner window, or an element in it. /// If used, sets = false. Else sets = true, unless is false or the active window belongs to this process. /// /// true if pressed OK. public bool ShowDialog(DependencyObject owner = null) { _b.R.AddOkCancel(_ok ?? "OK", _cancel ?? "Cancel"); _b.End(); var w = _b.Window; if (owner != null) w.ShowInTaskbar = false; else if (dialog.options.topmostIfNoOwnerWindow && !wnd.active.IsOfThisProcess) w.Topmost = true; if (!_b.ShowDialog(owner)) return false; BitArray ba = new(_ac.Count); int n = 0; for (int i = 0; i < ba.Length; i++) if (ba[i] = _ac[i].IsChecked == true) n++; ResultIndices = new int[n]; for (int i = 0, j = 0; i < ba.Length; i++) if (ba[i]) ResultIndices[j++] = i; ResultItems = new string[n]; for (int i = 0, j = 0; i < ba.Length; i++) if (ba[i]) ResultItems[j++] = _ac[i].Tag as string; ResultBits = ba; return true; } /// /// Gets a bit array where elements represent checkbox states (true if checked). /// This property is set by . /// public BitArray ResultBits { get; private set; } /// /// Gets 0-based indices of checked items. /// This property is set by . /// public int[] ResultIndices { get; private set; } /// /// Gets strings of checked items. /// This property is set by . /// /// /// a = ["one", "two"]; /// var d = new CheckListDialog("Info."); /// d.Add(a); /// if (!d.ShowDialog() || !d.ResultItems.Any()) return; /// a = d.ResultItems.ToList(); /// ]]> /// public string[] ResultItems { get; private set; } } ================================================ FILE: Au/Au.More/ComUtil.cs ================================================ namespace Au.More; /// /// COM utility functions. /// public static class ComUtil { /// /// Gets a COM object existing in other process and registered in ROT. /// /// ProgID of the COM class. Example: "Excel.Application". /// If fails, don't throw exception but return null. /// ///
progID not found in the registry. Probably it is incorrect, or the program isn't installed. ///
• An object of this type currently is unavailable. Probably the program is not running, or running with a different UAC integrity level. ///
/// /// Calls API GetActiveObject. /// /// This process must have the same [](xref:uac) integrity level as the target process (of the COM object). In script Properties select uac user. /// /// /// /// /// public static object GetActiveObject(string progID, bool dontThrow = false) { int hr = Api.CLSIDFromProgID(progID, out var clsid); if (hr < 0) return dontThrow ? null : throw Marshal.GetExceptionForHR(hr); return GetActiveObject(clsid, dontThrow); } /// An object of this type currently is unavailable. Probably its program is not running, or running with a different UAC integrity level. public static object GetActiveObject(in Guid clsid, bool dontThrow = false) { int hr = Api.GetActiveObject(clsid, default, out var r); if (hr < 0) return dontThrow ? null : throw Marshal.GetExceptionForHR(hr); return r; } /// /// Gets COM object from a window using API AccessibleObjectFromWindow(OBJID_NATIVEOM, IID_IDispatch). /// /// Window or control. /// Child window class name. Format: [wildcard expression](xref:wildcard_expression). If used, gets COM object from the first found child or descendant window where it succeeds. If null, gets COM object from w. /// If fails to get COM object, don't throw exception but return null. /// w 0 or invalid. /// Failed. /// /// Microsoft Excel. /// /// Microsoft Word. /// /// Microsoft PowerPoint. /// /// Microsoft Access. /// /// To get COM object from Microsoft Outlook or Publisher, use . /// public static object GetWindowObject(wnd w, string cnChild = null, bool dontThrow = false) { w.ThrowIfInvalid(); if (cnChild != null) { foreach (var c in w.ChildAll(cn: cnChild)) { if (0 == Api.AccessibleObjectFromWindow(c, EObjid.NATIVEOM, IID_IDispatch, out var o)) return o; } } else { if (0 == Api.AccessibleObjectFromWindow(w, EObjid.NATIVEOM, IID_IDispatch, out var o)) return o; } if (!dontThrow) throw new AuException($"*get COM object from window {w}"); return null; } static readonly Guid IID_IDispatch = new("00020400-0000-0000-C000-000000000046"); /// /// Creates COM object using ProgID. /// /// The programmatic identifier (ProgID) of the COM type. /// /// /// Use this function when you don't have the COM interface definition or the interop assembly. Else use code like var x = new InterfaceType(); or var x = new CoclassType() as InterfaceType. /// /// /// /// public static object CreateObject(string progID) { return Activator.CreateInstance(Type.GetTypeFromProgID(progID, throwOnError: true)); } /// /// Creates COM object using CLSID. /// /// public static object CreateObject(in Guid clsid) { return Activator.CreateInstance(Type.GetTypeFromCLSID(clsid, throwOnError: true)); } /// /// Default value for optional parameters of type VARIANT (object in C#) of COM functions. /// The same as . /// public static readonly System.Reflection.Missing Missing = System.Reflection.Missing.Value; } ================================================ FILE: Au/Au.More/Convert2.cs ================================================ using System.IO.Compression; using System.Security.Cryptography; namespace Au.More; /// /// Data conversion functions. Compression, encryption, hex encoding, etc. /// public static unsafe class Convert2 { #region hex /// /// Converts data in byte[] or other memory to hex-encoded string. /// /// Data. See also: , . /// Let the hex string contain A-F, not a-f. /// /// The result string length is 2 * data length. /// Often it's better to use , then result is 4/3 of data length. But cannot use Base64 in file names and URLs because it is case-sensitive and may contain character '/'. Both functions are fast. /// public static string HexEncode(RByte data, bool upperCase = false) { fixed (byte* p = data) { return HexEncode(p, data.Length, upperCase); } } /// /// Converts binary data in any memory to hex-encoded string. /// /// The data. Can be any valid memory of specified size, for example a struct address. /// data memory size (bytes). /// public static string HexEncode(void* data, int size, bool upperCase = false) { int u = (upperCase ? 'A' : 'a') - 10; var bytes = (byte*)data; string R = new('\0', size * 2); fixed (char* cp = R) { int* p = (int*)cp; for (int i = 0; i < size; i++) { int b = bytes[i]; int t = b & 0xf; t += t < 10 ? '0' : u; b >>= 4; b += b < 10 ? '0' : u; p[i] = t << 16 | b; } } return R; //tested: ~50% of time takes string allocation. //tested: with string.Create same speed. } /// /// Converts a struct variable to hex-encoded string. /// /// Variable. /// public static string HexEncode(T x, bool upperCase = false) where T : unmanaged => HexEncode(&x, sizeof(T), upperCase); /// /// Converts hex-encoded string to binary data. /// /// String or char[] or span of string/array/memory containing hex encoded data. /// /// Skips spaces and other non-hex-digit characters. Example: "01 23 46 67" is the same as "01234667". /// The number of hex digit characters should be divisible by 2, else the last character is ignored. /// [SkipLocalsInit] public static byte[] HexDecode(RStr encoded) { using FastBuffer b = new(encoded.Length / 2); int n = HexDecode(encoded, b.p, b.n); var r = new byte[n]; Marshal.Copy((IntPtr)b.p, r, 0, n); return r; } /// /// Converts hex-encoded string to binary data. Writes to caller's memory buffer. /// /// The number of bytes written in decoded memory. It is equal or less than Math.Min(bufferSize, encoded.Length/2). /// Memory buffer for result. /// Max number of bytes that can be written to the decoded memory buffer. /// public static int HexDecode(RStr encoded, void* decoded, int bufferSize) { if (encoded.Length == 0) return 0; var t = Tables_.Hex; byte* r = (byte*)decoded, rTo = r + bufferSize; uint k = 1; for (int i = 0; i < encoded.Length; i++) { uint c = (uint)(encoded[i] - '0'); if (c >= 55) continue; c = t[c]; if (c == 0xFF) continue; k <<= 4; k |= c; if (0 != (k & 0x100)) { if (r >= rTo) break; *r++ = (byte)k; k = 1; } } return (int)(r - (byte*)decoded); //speed: slightly slower than Base64Decode for the same binary data size. } /// /// Converts hex-encoded string to a struct variable. /// /// false if decoded size != sizeof(T). /// The result variable. /// public static bool HexDecode(RStr encoded, out T decoded) where T : unmanaged { T t; if (HexDecode(encoded, &t, sizeof(T)) != sizeof(T)) { decoded = default; return false; } decoded = t; return true; } #endregion #region compress /// /// Compresses data. Uses . /// /// Data. See also: , . /// Exceptions of . public static byte[] DeflateCompress(RByte data) { using var m = new MemoryStream(); using (var x = new DeflateStream(m, CompressionLevel.Optimal)) x.Write(data); //note: must dispose before ToArray return m.ToArray(); //tested: GZipStream same compression but adds 18 bytes header. DeflateStream does not add any header. //tested: bz2 and 7z compression isn't much better with single 15 kb bmp file. } /// /// Decompresses data. Uses . /// /// Decompressed data. /// Compressed data. /// Exceptions of . public static byte[] DeflateDecompress(RByte compressed) { using var m = new MemoryStream(); DeflateDecompress(compressed, m); return m.ToArray(); } /// /// Decompresses data to a caller-provided memory stream. Uses . /// /// Compressed data. /// Stream for decompressed data. /// Exceptions of . public static void DeflateDecompress(RByte compressed, Stream decompressed) { fixed (byte* p = compressed) { using var m = new UnmanagedMemoryStream(p, compressed.Length); using var x = new DeflateStream(m, CompressionMode.Decompress); x.CopyTo(decompressed); } //note: cannot deflateStream.Read directly to array because its Length etc are not supported. //note: also cannot use decompressedStream.GetBuffer because it can be bigger. } /// /// Compresses data. Uses . /// /// Data. See also: , . /// Exceptions of . public static byte[] GzipCompress(RByte data) { using var m = new MemoryStream(); using (var x = new GZipStream(m, CompressionLevel.Optimal)) x.Write(data); return m.ToArray(); } /// /// Decompresses data. Uses . /// /// Decompressed data. /// Compressed data. /// Exceptions of . public static byte[] GzipDecompress(RByte compressed) { using var m = new MemoryStream(); GzipDecompress(compressed, m); return m.ToArray(); } /// /// Decompresses data to a caller-provided memory stream. Uses . /// /// Compressed data. /// Stream for decompressed data. /// Exceptions of . public static void GzipDecompress(RByte compressed, Stream decompressed) { fixed (byte* p = compressed) { using var m = new UnmanagedMemoryStream(p, compressed.Length); using var x = new GZipStream(m, CompressionMode.Decompress); x.CopyTo(decompressed); } } /// /// Compresses data. Uses . /// /// Data. See also: , . /// Compression level, 0 (no compression) to 11 (maximal compression). Default 6. Bigger levels don't make much smaller but can make much slower. /// Invalid level. /// /// failed. public static unsafe byte[] BrotliCompress(RByte data, int level = 6) { int n = BrotliEncoder.GetMaxCompressedLength(data.Length) + data.Length / 1000 + 1000; //GetMaxCompressedLength returns too small value, and TryCompress fails when data is already compressed var b = MemoryUtil.Alloc(n); try { if (!BrotliEncoder.TryCompress(data, new(b, n), out n, level, 22)) throw new AuException(); return new Span(b, n).ToArray(); } finally { MemoryUtil.Free(b); } } /// /// Decompresses data. Uses . /// /// Decompressed data. /// Compressed data. /// Invalid data. /// public static unsafe byte[] BrotliDecompress(RByte compressed) { int n = checked(compressed.Length * 4 + 8000); for (int i = 0; i < 3; i++) if (n < 512_000) n *= 2; //print.it(compressed.Length, n, n/compressed.Length); //usually ~ 80 KB for (; ; n = checked(n * 2)) { byte* b = MemoryUtil.Alloc(n); try { if (BrotliDecoder.TryDecompress(compressed, new(b, n), out int nw)) return new Span(b, nw).ToArray(); if (nw == 0) throw new ArgumentException("cannot decompress this data"); //print.it(n); } finally { MemoryUtil.Free(b); } } } #endregion #region encrypt /// /// AES-encrypts a byte[] or string. Returns byte[]. /// /// Data to encrypt. Can be byte[] or string. /// Encryption key. Can be non-empty string (eg a password) or byte[] of length 16 (eg a password hash). /// Encrypted data. The first 16 bytes is initialization vector (not secret). /// /// public static byte[] AesEncryptB(object data, object key) { Not_.Null(data, key); var enc = AesEncryptB(data, key, out var iv); var r = new byte[enc.Length + iv.Length]; iv.CopyTo(r, 0); enc.CopyTo(r, iv.Length); return r; } /// /// AES-encrypts a byte[] or string. Returns byte[]. /// /// Data to encrypt. Can be byte[] or string. /// Encryption key. Can be non-empty string (eg a password) or byte[] of length 16 (eg a password hash). /// Receives an initialization vector. The function generates a random value. Use it with decrypt functions. Don't need to keep it in secret. /// Encrypted data. /// /// public static byte[] AesEncryptB(object data, object key, out byte[] IV) { Not_.Null(data, key); var d = data switch { byte[] b => b, string s => Encoding.UTF8.GetBytes(s), _ => throw new ArgumentException("Expected byte[] or string", "data") }; using var aes = Aes.Create(); using var x = aes.CreateEncryptor(_Key(key), IV = aes.IV); return x.TransformFinalBlock(d, 0, d.Length); } /// /// AES-encrypts a byte[] or string. /// Calls and converts the returned byte[] to Base64 string. /// /// /// /// /// public static string AesEncryptS(object data, object key) { Not_.Null(data, key); return Convert.ToBase64String(AesEncryptB(data, key)); } /// /// AES-encrypts a byte[] or string. /// Calls and converts the returned byte[] to Base64 string. /// /// public static string AesEncryptS(object data, object key, out byte[] IV) { Not_.Null(data, key); return Convert.ToBase64String(AesEncryptB(data, key, out IV)); } /// /// AES-decrypts data. Returns byte[]. /// /// Encryped data as byte[] or Base64 string. /// Encryption key. Can be non-empty string (eg a password) or byte[] of length 16 (eg a password hash). /// If used AesEncryptX that returns an initialization vector, pass it here. /// /// public static byte[] AesDecryptB(object data, object key, byte[] IV = null) { Not_.Null(data, key); var d = data switch { byte[] b => b, string s => Convert.FromBase64String(s), _ => throw new ArgumentException("Expected byte[] or string", "data") }; using var aes = Aes.Create(); using var x = aes.CreateDecryptor(_Key(key), IV ?? d[..16]); var (from, len) = IV != null ? (0, d.Length) : (16, d.Length - 16); return x.TransformFinalBlock(d, from, len); } /// /// AES-decrypts data. /// Calls and converts the returned byte[] to string. /// /// public static string AesDecryptS(object data, object key, byte[] IV = null) { Not_.Null(data, key); return Encoding.UTF8.GetString(AesDecryptB(data, key, IV)); } static byte[] _Key(object key) { switch (key) { case byte[] b: if (b.Length != 16) throw new ArgumentException("Expected length 16", "key"); return b; case string s: if (s.Length == 0) throw new ArgumentException("Empty string", "key"); return Hash.MD5(s).ToArray(); } throw new ArgumentException("Expected byte[] or string", "key"); } #endregion #region utf8 /// /// Converts string to UTF-8 byte[]. Can append "\0" (default) or some other string. /// /// String or char[] or span of string/array/memory. /// A string to append, or null. For example "\0" (default) or "\r\n". Must contain only ASCII characters. /// append contains non-ASCII characters. public static byte[] Utf8Encode(RStr chars, string append = "\0") { int n = Encoding.UTF8.GetByteCount(chars); int nAppend = append.Lenn(); var r = new byte[n + nAppend]; int nn = Encoding.UTF8.GetBytes(chars, r); Debug.Assert(nn == n); if (nAppend > 0 && !(nAppend == 1 && append[0] == '\0')) { foreach (char c in append) { if (c > 127) throw new ArgumentException("append must be ASCII"); r[nn++] = (byte)c; } } return r; //speed: faster than WideCharToMultiByte. Same as System.Text.Unicode.Utf8.FromUtf16. } /// /// Converts '\0'-terminated UTF-8 string to C# string (UTF-16). /// /// UTF-8 string. If null, returns null. /// /// Finds '\0' and calls . Don't use this function when UTF-8 string length is known; call Encoding.UTF8.GetString directly. /// public static string Utf8Decode(byte* utf8) => utf8 == null ? null : Encoding.UTF8.GetString(utf8, Ptr_.Length(utf8)); /// /// Converts string to UTF-8. If non-ASCII, gets UTF-8 character offsets. /// /// /// Tuple: ///
text - Encoding.UTF8.GetBytes(s). ///
offsets - null if s is ASCII. Else UTF-8 character offsets for each s character plus at s.Length. ///
internal static (byte[] text, int[] offsets) Utf8EncodeAndGetOffsets_(RStr s, bool append0 = false) { var s2 = Utf8Encode(s, append0 ? "\0" : ""); //always creates valid UTF-8. Replaces invalid UTF-16 chars. int len2 = s2.Length; if (append0) len2--; if (len2 == s.Length) return (s2, null); //ASCII var a = new int[s.Length + 1]; int i8 = 0, i16 = 0; while (i8 < len2) { a[i16++] = i8; int c = s2[i8]; if (c < 0xC0) i8++; else if (c < 0xE0) i8 += 2; else if (c < 0xF0) i8 += 3; else { a[i16++] = i8; i8 += 4; } } Debug.Assert(i8 == len2); Debug.Assert(i16 == s.Length); a[i16] = i8; return (s2, a); } #endregion #region base64 (rejected) ///// ///// Gets Base64 encoded string length for non-encoded length. ///// It is (length + 2) / 3 * 4. ///// //public static int Base64EncodeLength(int length) => checked((length + 2) / 3 * 4); ///// ///// Gets decoded data length from Base64 encoded string length, assuming there are no newlines and other whitespace characters. ///// It is (int)(len * 3L / 4) minus the number of padding '=' characters (max 2). ///// //public static int Base64DecodeLength(RStr encoded) { // int len = encoded.Length; // if (len == 0) return 0; // int r = (int)(len * 3L / 4); // if (0 == (len & 3)) { // if (encoded[len - 1] == '=') { // r--; // if (encoded[len - 2] == '=') r--; // } // } // return r; //} //currently not used in this lib. Not tested speed. ///// ///// Converts string span containing Base64 encoded data to byte[]. ///// ///// String or char[] or span of string/array/memory containing Base64 encoded data. ///// ///// false if failed. ///// ///// Uses . ///// //public static bool TryBase64Decode(RStr encoded, out byte[] result) { // result = null; // encoded = encoded.Trim(); // //todo: calc length: skip whitespace. Maybe bool parameter. Or use FastBuffer and copy, like in HexDecode. // var a = new byte[Base64DecodeLength(encoded)]; // if (!Convert.TryFromBase64Chars(encoded, a, out int len)) return false; // if (len != a.Length) { // Debug_.Print($"{a.Length} {len}"); // a = a.AsSpan(0, len).ToArray(); // } // result = a; // return true; //} //rejected: URL-safe Base64. // Not used in this lib. // Cannot be used in file names because case-sensitive. // If somebody wants URL-safe Base64, it's easy/fast to replace unsafe characters. Nobody would find and use these functions. ///// ///// Converts byte[] or other memory to Base64 encoded string that can be used in URL. ///// ///// Data to encode. ///// Like , but instead of '/' and '+' uses '_' and '-'. //public static string Base64UrlEncode(RByte data) { // fixed (byte* p = data) return Base64UrlEncode(p, data.Length); // //speed: same as Convert.ToBase64String //} ///// ///// Converts binary data stored in any memory to Base64 encoded string that can be used in URL. ///// ///// Data to encode. ///// Number of bytes to encode. ///// Instead of '/' and '+' uses '_' and '-'. //public static string Base64UrlEncode(void* data, int length) { // var ip = (IntPtr)data; // return string.Create(Base64EncodeLength(length), (ip, length), static (span, tu) => { // Convert.TryToBase64Chars(new RByte((byte*)tu.ip, tu.length), span, out _); // for (int i = 0; i < span.Length; i++) { // switch (span[i]) { case '/': span[i] = '_'; break; case '+': span[i] = '-'; break; } // } // }); //} ///// ///// Converts a struct variable to Base64 encoded string that can be used in URL. ///// ///// Variable. ///// Instead of '/' and '+' uses '_' and '-'. //public static string Base64UrlEncode(T x) where T : unmanaged { // return Base64UrlEncode(&x, sizeof(T)); //} //static void _Base64_Replace(RStr encoded, char[] a) { // for (int i = 0; i < encoded.Length; i++) { // char c = encoded[i]; // a[i] = c switch { '_' => '/', '-' => '+', _ => c, }; // } //} ///// ///// Converts string containing Base64 encoded data to byte[]. Supports standard encoding and URL-safe encoding. ///// ///// String or char[] or span of string/array/memory containing Base64 encoded data. ///// Like , but the string can contain '_' and '-' instead of '/' and '+'. ///// Exceptions of . //public static byte[] Base64UrlDecode(RStr encoded) { // char[] a = ArrayPool.Shared.Rent(encoded.Length); // try { // _Base64_Replace(encoded, a); // return Convert.FromBase64CharArray(a, 0, encoded.Length); // } // finally { ArrayPool.Shared.Return(a); } // //never mind: almost 2 times slower than Convert.FromBase64CharArray. // // Normally this func is used with short strings and not with many strings in loop. // // ArrayPool isn't as fast as should be. And copying to new array takes time. //} ///// ///// Converts string containing Base64 encoded data to bytes and stores in memory of a Span variable. Supports standard encoding and URL-safe encoding. ///// Returns false if the encoded string is invalid or the buffer is too small. ///// ///// String or char[] or span of string/array/memory containing Base64 encoded data. ///// Memory buffer for the result. ///// ///// The string can contain '_' and '-' instead of '/' and '+'. //public static bool Base64UrlDecode(RStr encoded, Span decoded, out int decodedLength) { // char[] a = ArrayPool.Shared.Rent(encoded.Length); // try { // _Base64_Replace(encoded, a); // return Convert.TryFromBase64Chars(new RStr(a, 0, encoded.Length), decoded, out decodedLength); // } // finally { ArrayPool.Shared.Return(a); } //} ///// ///// Converts string containing Base64 encoded data to bytes and stores in any memory. Supports standard encoding and URL-safe encoding. ///// Returns false if the encoded string is invalid or the buffer is too small. ///// ///// String or char[] or span of string/array/memory containing Base64 encoded data. ///// Memory buffer for the result. ///// The max number of bytes that can be written to the decoded memory buffer. ///// Receives the number of bytes written to the decoded memory buffer. ///// The string can contain '_' and '-' instead of '/' and '+'. //public static bool Base64UrlDecode(RStr encoded, void* decoded, int bufferSize, out int decodedLength) { // return Base64UrlDecode(encoded, new Span(decoded, bufferSize), out decodedLength); //} ///// ///// Converts string containing Base64 encoded data to a struct variable. Supports standard encoding and URL-safe encoding. ///// Returns false if the encoded string is invalid or decoded size != sizeof(T). ///// ///// String or char[] or span of string/array/memory containing Base64 encoded data. ///// The result variable. ///// The string can contain '_' and '-' instead of '/' and '+'. //public static bool Base64UrlDecode(RStr encoded, out T decoded) where T : unmanaged { // T t; // if (!Base64UrlDecode(encoded, &t, sizeof(T), out int n) || n != sizeof(T)) { decoded = default; return false; } // decoded = t; // return true; //} #endregion } ================================================ FILE: Au/Au.More/DebugTraceListener.cs ================================================ namespace Au.More; /// /// Replaces the default trace listener with a listener that shows a message box on a failed assertion. /// /// /// The new trace listener overrides the method. /// On failed assertion (, , , ) it shows a message box with buttons Exit Debug Ignore, unless debugger is attached or is false. /// public class DebugTraceListener : DefaultTraceListener { /// /// Replaces default trace listener. /// /// Also set = true. //[Conditional("DEBUG"), Conditional("TRACE")] //no, in most cases this is called by this library, not directly by the app public static void Setup(bool usePrint) { if (!s_setup) { s_setup = true; Trace.Listeners.Remove("Default"); //remove DefaultTraceListener. It calls Environment.FailFast which shows message box "Unknown hard error". Trace.Listeners.Add(new DebugTraceListener()); } print.redirectDebugOutput = usePrint; } static bool s_setup; /// public override void Fail(string message, string detailMessage) { var s = message; if (s.NE()) s = detailMessage; else if (!detailMessage.NE()) s = message + "\r\n" + detailMessage; if (!s.NE()) s += "\r\n"; string st = new StackTrace(2, true).ToString(), st1 = null; if (st.RxMatch(@"(?m)^\s+at (?!System\.Diagnostics\.)", 0, out RXGroup g)) { st = st[g.Start..]; st1 = st.Lines(true)[0]; } var s2 = "---- Debug assertion failed ----\r\n" + s + st; Trace.WriteLine(s2); if (!(print.redirectDebugOutput && print.qm2.use)) print.qm2.write(s2); if (Debugger.IsAttached) { Debugger.Break(); } else { if (!AssertUiEnabled) return; //like default listener s = $"{s}{st1}\n\nProcess id: {process.thisProcessId}"; //int r = dialog.showWarning("Debug assertion failed", s, "1 Exit|2 Ignore|3 script.debug|4 Debugger.Launch", expandedText: st); //no. Need to block all messages in this thread, to prevent reentering this or executing code somewhere else. //int r = Task.Run(() => dialog.showWarning("Debug assertion failed", s, "1 Exit|2 Ignore|3 script.debug|4 Debugger.Launch", expandedText: st)).Result; //no. It seems .NET adds messages (eg WM_TIMER) to a queue, and finally reposts all. int r = 0; var thread = run.thread(() => { r = dialog.showWarning("Debug assertion failed", s, "1 Exit|2 Ignore|3 script.debug|4 Debugger.Launch", expandedText: st); }); do 100.ms(); while (thread.IsAlive); if (r == 1) Api.ExitProcess(-1); if (r == 2) return; if (r == 4) Debugger.Launch(); else { script.debug(); Debugger.Break(); } } } } ================================================ FILE: Au/Au.More/Dpi.cs ================================================ namespace Au.More { /// /// Functions for high-DPI screen support. /// /// /// To find DPI % on Windows 10 and 11: Settings > System > Display > Scale and layout. If not 100%, it means high DPI. /// /// This program must be per-monitor-DPI-aware. Else results are undefined. /// public static class Dpi { /// /// Gets DPI of the primary screen at the time this process started. /// public static int System { get { if (_systemDPI == 0) { using var dcs = new ScreenDC_(); _systemDPI = Api.GetDeviceCaps(dcs, 90); //LOGPIXELSY //could use GetDpiForSystem instead (Windows 10 1607). // Normally the same result (tested), but probably not if thread awareness context is Unaware (not tested). } return _systemDPI; } } static int _systemDPI; /// /// Gets the DPI of a window, as used in the window's process. It never changes for that window instance. /// /// If failed, returns the system DPI (). /// A top-level window or control. Can belong to any process. /// /// If true, works on Windows 8.1 and later; however on Windows 8.1 slower and less reliable. /// If false (default), works on Windows 10 1607 and later. /// /// /// The result depends on the DPI awareness of the window: /// - per-monitor-DPI-aware - usually DPI of the windows's screen. /// - system aware - system DPI (DPI of the primary screen). /// - unaware - 96. /// /// The result also depends on the Windows version: /// - Works best on Windows 10 1607 and later. Uses API GetDpiForWindow. /// - On Windows 8.1 works if supportWin81 true. If false (default), returns . /// - On Windows 7 and 8.0 always returns , because there are no Windows API. Most apps are system-DPI-aware and the result is correct; for unaware apps the result is incorrect. These Windows versions don't support per-monitor DPI. /// public static int OfWindow(wnd w, bool supportWin81 = false) { if (!osVersion.minWin8_1) return System; int R = 0; if (!w.Is0) { if (osVersion.minWin10_1607) R = Api.GetDpiForWindow(w); else if (supportWin81) { var v = WindowDpiAwareness(w); //info: quickly returns Awareness.PerMonitor if w.IsOfThisProcess if (v == Awareness.Unaware) R = 96; else if (v == Awareness.PerMonitor) if (0 != Api.GetDpiForMonitor(Api.MonitorFromWindow(w.Window, SODefault.Nearest), 0, out R, out _)) R = 0; } } return R != 0 ? R : System; } /// /// Returns OfWindow(w.Hwnd()). /// /// A Winforms window or control. public static int OfWindow(System.Windows.Forms.Control w) => OfWindow(w.Hwnd()); //rejected: supportWin81 /// /// Returns OfWindow(w.Hwnd()). /// /// A WPF window or element. public static int OfWindow(System.Windows.DependencyObject w) => OfWindow(w.Hwnd()); /// /// Gets DPI of a screen. /// /// if fails or if not supported on this Windows version. /// Native screen handle (HMONITOR). /// Support Windows 8.1 and later. If false (default), supports Windows 10 1607 and later. /// /// Uses API GetDpiForMonitor. /// /// public static int OfScreen(IntPtr hMonitor, bool supportWin81 = false) { bool os = supportWin81 ? osVersion.minWin8_1 : osVersion.minWin10_1607; return os && 0 == Api.GetDpiForMonitor(hMonitor, 0, out int R, out _) ? R : System; } ///// //public static int OfScreen(screen s, bool supportWin81 = false) => OfScreen(s.Now, supportWin81); /// /// Scales int if the specified DPI isn't 96 (100%). /// public static int Scale(int i, DpiOf dpiOf) => Math2.MulDiv(i, dpiOf, 96); //no. Eg also would be used for uint, long... Or name eg ScaleD. Or add double extension method. ///// ///// Scales int if the specified DPI isn't 96 (100%). ///// //public static int Scale(double i, DpiOf dpiOf) => (i*(int)dpiOf/96).ToInt(); /// /// Unscales int if the specified DPI isn't 96 (100%). /// public static double Unscale(int i, DpiOf dpiOf) => i * (96d / dpiOf); //Unscaling sometimes useful with WPF. Unscale to double, not int, else result often incorrect. /// /// Scales if the specified DPI isn't 96 (100%). /// public static SIZE Scale(SIZE z, DpiOf dpiOf) { int dpi = dpiOf; z.width = Math2.MulDiv(z.width, dpi, 96); z.height = Math2.MulDiv(z.height, dpi, 96); return z; } /// /// Scales if the specified DPI isn't 96 (100%). /// public static SIZE Scale(System.Windows.Size z, DpiOf dpiOf) { double f = (int)dpiOf / 96d; z.Width *= f; z.Height *= f; return new(z.Width.ToInt(), z.Height.ToInt()); } /// /// Unscales if the specified DPI isn't 96 (100%). /// public static System.Windows.Size Unscale(SIZE z, DpiOf dpiOf) { double f = 96d / dpiOf; return new(z.width * f, z.height * f); } /// /// Scales if the specified DPI isn't 96 (100%). /// public static RECT Scale(RECT r, DpiOf dpiOf) { int dpi = dpiOf; r.left = Math2.MulDiv(r.left, dpi, 96); r.top = Math2.MulDiv(r.top, dpi, 96); r.right = Math2.MulDiv(r.right, dpi, 96); r.bottom = Math2.MulDiv(r.bottom, dpi, 96); return r; } /// /// Unscales if the specified DPI isn't 96 (100%). /// public static System.Windows.Rect Unscale(RECT r, DpiOf dpiOf) { double f = 96d / dpiOf; return new(r.left * f, r.top * f, r.Width * f, r.Height * f); } /// /// Calls API GetSystemMetricsForDpi if available, else GetSystemMetrics. /// public static int GetSystemMetrics(int nIndex, DpiOf dpiOf) => osVersion.minWin10_1607 ? Api.GetSystemMetricsForDpi(nIndex, dpiOf) : Api.GetSystemMetrics(nIndex); /// /// Calls API SystemParametersInfoForDpi if available, else SystemParametersInfo. /// Use only with uiAction = SPI_GETICONTITLELOGFONT, SPI_GETICONMETRICS, SPI_GETNONCLIENTMETRICS. /// public static unsafe bool SystemParametersInfo(uint uiAction, int uiParam, void* pvParam, DpiOf dpiOf) => osVersion.minWin10_1607 ? Api.SystemParametersInfoForDpi(uiAction, uiParam, pvParam, 0, dpiOf) : Api.SystemParametersInfo(uiAction, uiParam, pvParam); /// /// Calls API AdjustWindowRectExForDpi if available, else AdjustWindowRectEx. /// /// /// Also adds scrollbar width or/and height if need. /// public static bool AdjustWindowRectEx(DpiOf dpiOf, ref RECT r, WS style, WSE exStyle, bool hasMenu = false) { int dpi = dpiOf; bool ok = osVersion.minWin10_1607 ? Api.AdjustWindowRectExForDpi(ref r, style, hasMenu, exStyle, dpi) : Api.AdjustWindowRectEx(ref r, style, hasMenu, exStyle); if (ok) { if (style.Has(WS.VSCROLL)) r.Width += ScrollbarV_(dpi); if (style.Has(WS.HSCROLL)) r.Width += ScrollbarH_(dpi); } return ok; } internal static int ScrollbarV_(int dpi) => GetSystemMetrics(Api.SM_CXVSCROLL, dpi); internal static int ScrollbarH_(int dpi) => GetSystemMetrics(Api.SM_CYHSCROLL, dpi); /// /// DPI awareness of a window or process. /// public enum Awareness { //== DPI_AWARENESS == PROCESS_DPI_AWARENESS /// Invalid = -1, /// Unaware, /// System, /// PerMonitor } /// /// Gets DPI awareness of a window. /// /// if failed. /// A top-level window or control. Can belong to any process. /// /// Works best on Windows 10 1607 and later; uses API GetWindowDpiAwarenessContext. /// On Windows 8.1 returns if w is of this process; else uses API GetProcessDpiAwareness, which is slower and less reliable. /// On Windows 7 and 8.0 always returns , because there are no Windows API. /// public static Awareness WindowDpiAwareness(wnd w) { if (osVersion.minWin10_1607) { return Api.GetAwarenessFromDpiAwarenessContext(Api.GetWindowDpiAwarenessContext(w)); } else if (osVersion.minWin8_1) { if (w.IsOfThisProcess) return Awareness.PerMonitor; using var hp = Handle_.OpenProcess(w); return (!hp.Is0 && 0 == Api.GetProcessDpiAwareness(hp, out var a)) ? a : Awareness.Invalid; } else { return Awareness.System; //could use, IsWindowVirtualized (except if of this process), but slow and unreliable. } } /// /// Detects whether the window is DPI-scaled/virtualized. /// /// false if not DPI-scaled/virtualized or if fails to detect or if invalid window handle. /// A top-level window or control. Can belong to any process. /// /// Such windows are a little blurry (entire client area). /// /// OS scales a window when it is on a high-DPI screen, depending on the DPI awareness of the window: /// - Unaware - always. /// - System - if the screen DPI is not equal to the system DPI of that process (which usually is of the primary screen, but not always). /// /// Such windows have various problems for automation apps: /// - Often difficult or impossible to get correct rectangles of UI elements (and therefore cannot click etc) or get correct UI element from point. It depends on used API (UIA, MSAA, JAB), inproc/notinproc, OS version and application. /// - On Windows 7 and 8.0 cannot easily get correct rectangles of such windows and their controls. This library ignores it, because would be too much work to apply workarounds in so many places and just for legacy OS versions (it has been fixed in Windows 8.1). /// - If with want to use window pixels, need to capture image from window pixels, not from screen pixels. /// /// This function is not completely reliable. And not very fast. This process must be per-monitor-DPI-aware. /// public static bool IsWindowVirtualized(wnd w) { if (GetScalingInfo_(w, out bool scaled, out _, out _)) return scaled; return IsWindowVirtualizedLegacy_(w); //note: child windows can have different DPI awareness (minWin10_1607). See GetWindowDpiHostingBehavior. Not tested, not seen. //Also tested detecting with GDI. GDI functions return logical (not DPI-scaled) offsets/rectangles/etc. Works, but much slower. } /// /// On Win10+ returns . Else false. /// internal static bool IsWindowVirtualizedWin10_(wnd w) => osVersion.minWin10_1607 && IsWindowVirtualized(w); /// /// If possible, gets whether the window is DPI-scaled/virtualized, and gets physical and logical rects if scaled. /// Returns false if !osVersion.minWin10_1607 or if cannot get that info. /// Gets that info in a fast and reliable way. /// internal static bool GetScalingInfo_(wnd w, out bool scaled, out RECT rPhysical, out RECT rLogical) { scaled = false; rPhysical = default; rLogical = default; if (!osVersion.minWin10_1607) return false; var awareness = WindowDpiAwareness(w); //fast on Win10 if (awareness is Awareness.System or Awareness.Unaware) { //tested: unaware-gdi-scaled same as unaware if (awareness == Awareness.System && Api.GetDpiForWindow(w) != System) { /*fast*/ //Cannot get rLogical. It's rare and temporary, ie when the user recently changed DPI of the primary screen. //Even if this func isn't used to get rects, without this fast code could be unreliable. Debug_.Print("w System DPI != our System DPI"); return false; } for (; ; ) { RECT r1 = w.Rect, r2, r3; //note: with ClientRect 4 times faster, but unreliable if small rect. Now fast enough. bool rectWorkaround = false; using (var u = new AwarenessContext(awareness == Awareness.System ? -2 : -1)) { if (Api.GetAwarenessFromDpiAwarenessContext(u.Previous_) != Awareness.PerMonitor) { /*fast*/ //cannot get rPhysical. But let's set PM awareness and get it. Works even if this process is Unaware. rectWorkaround = _GetRect(w, out r1); Debug_.Print("bad DPI awareness of this thread; workaround " + (rectWorkaround ? "OK" : "failed")); if (!rectWorkaround) return false; //unlikely. Then the caller probably will call the legacy func, it works with any DPI awareness. } r2 = w.Rect; if (r2 == r1) break; } if (!rectWorkaround) r3 = w.Rect; else _GetRect(w, out r3); if (r3 != r1) continue; //moved, resized or closed between Rect and Rect scaled = true; rPhysical = r1; rLogical = r2; break; } static bool _GetRect(wnd w, out RECT r) { using (var u2 = new AwarenessContext(-4)) { //per-monitor-v2 if (u2.Previous_ == 0) { r = default; return false; } //API failed. Unlikely. Works even if this process is Unaware. r = w.Rect; } return true; } } return true; } internal static bool IsWindowVirtualizedLegacy_(wnd w) { //less reliable if control. // LogicalToPhysicalPoint fails if not in top-level window rect. // It seems PhysicalToLogicalPointForPerMonitorDPI doesn't fail, but it fails if not in that screen. w = w.Window; RECT rPrev = default; for (int i = 0; i < 5; i++) { if (!Api.GetWindowRect(w, out var r)) break; //Win10 1 mcs hot, 20 cold, old OS fast if (r != rPrev) { i = 0; rPrev = r; } //moved or resized POINT p = new(r.CenterX, r.CenterY); //p must be inside the window if (i == 1) p.y = r.bottom - 1; else if (i == 2) p.y = r.top; else if (i == 3) p.x = r.right - 1; else if (i == 4) p.x = r.left; //and p must be in correct screen, which is unknown and therefore we use this code to guess. Normally succeeds at i==0, but may fail when the window is more than in 1 screen. POINT k = p; if (osVersion.minWin8_1 ? Api.PhysicalToLogicalPointForPerMonitorDPI(w, ref p) : Api.LogicalToPhysicalPoint(w, ref p)) { //Win10 3 mcs hot, old OS fast //API bug: may scale the point although the window isn't scaled. Never mind. // When window's center is between 2 screens and at the same time half of the window is offscreen. The area is several pixels wide. //if (p != k) print.it(k, p); if (p != k) return true; break; } } return false; //tested: the API works with hidden and off-screen top-level windows too. Minimized windows are off-screen. //tested: works even if this process is DPI System or Unaware. Tested on Win10. //speed on Win10 when not PM-aware: 4 mcs hot, 40 mcs cold. All API much faster on old OS (tested on Vmware). } //No. In some cases (window positions) screen.of(w) does not match scaling. Not much faster. //public static bool IsWindowVirtualized2(wnd w) { // if (osVersion.minWin10_1607) { // if (WindowDpiAwareness(w) is Awareness.PerMonitor or Awareness.Invalid) return false; //fast on Win10; slow on Win8.1, unavailable on Win7/8. Other API very slow on Win10, much faster on old OS (Vmware). // w = w.Window; if (w.Is0) return false; //0.6 mcs hot // var m = screen.of(w); // int d1 = m.Dpi, d2 = OfWindow(w); // //if (d1 == d2) print.it(d1); // if (d1 == d2) return false; // return w.IsAlive; // } else { // //same as now // return false; // } //} /// /// Can be used to temporarily change thread's DPI awareness context with API SetThreadDpiAwarenessContext. /// Does nothing if the API is unavailable (added in Windows 10 version 1607). /// /// /// Programs that use this library should use manifest with dpiAwareness = PerMonitorV2 and dpiAware = True/PM. Then default DPI awareness context is DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2. /// /// /// /// public struct AwarenessContext : IDisposable { nint _dac; /// /// If , calls API SetThreadDpiAwarenessContext. /// /// One of DPI_AWARENESS_CONTEXT constants: -1 unaware, -2 system, -3 per-monitor, -4 per-monitor-v2, -5 unaware-gdiscaled. Or a DPI_AWARENESS_CONTEXT handle. public AwarenessContext(nint dpiContext) { _dac = osVersion.minWin10_1607 ? Api.SetThreadDpiAwarenessContext(dpiContext) : default; } //rejected: enum dpiContext. /// /// If , calls API GetWindowDpiAwarenessContext and SetThreadDpiAwarenessContext. Sets the awareness context of this thread equal to that of the window. /// public AwarenessContext(wnd w) : this(Available ? Api.GetWindowDpiAwarenessContext(w) : 0) { } /// /// Restores previous DPI awareness context. /// public void Dispose() { if (_dac == default) return; Api.SetThreadDpiAwarenessContext(_dac); _dac = default; } /// internal nint Previous_ => _dac; /// /// Returns true if thread DPI awareness contexts are available on this Windows version (Windows 10 1607 and later). /// public static bool Available => osVersion.minWin10_1607; ///// //public const nint DPI_AWARENESS_CONTEXT_UNAWARE = -1; ///// //public const nint DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = -2; ///// //public const nint DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = -3; ///// //public const nint DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4; ///// //public const nint DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED = -5; } } } namespace Au.Types { /// /// Used for dpiOf parameter of functions. Allows to pass either a DPI value or an object from which gets DPI (window, screen etc). /// /// /// Has implicit conversions from: ///
int - DPI. ///
- gets DPI of the window. ///
IntPtr or nint - (screen handle) gets DPI of the screen. ///
- gets DPI of the screen that contains the point. ///
- gets DPI of screen that contains the rectangle. ///
- gets DPI of the winforms control. ///
- gets DPI of the WPF element. /// /// The conversion operators set the property and the function can use it. ///
public struct DpiOf { readonly int _dpi; /// public DpiOf(int dpi) { _dpi = dpi; } /// Invalid window handle. public DpiOf(wnd w) { w.ThrowIfInvalid(); _dpi = More.Dpi.OfWindow(w); } //rejected: parameter supportWin81. Rarely used. Can use Dpi functions when need. /// /// Invalid window handle. public DpiOf(System.Windows.Forms.Control c) : this(Not_.NullRet(c).Hwnd()) { } /// /// Invalid window handle. public DpiOf(System.Windows.DependencyObject c) : this(Not_.NullRet(c).Hwnd()) { } /// public DpiOf(IntPtr hMonitor) { _dpi = More.Dpi.OfScreen(hMonitor); } /// public DpiOf(POINT screenOf) : this(screen.of(screenOf).Handle) { } /// public DpiOf(RECT screenOf) : this(screen.of(screenOf).Handle) { } /// public static implicit operator DpiOf(int dpi) => new(dpi); /// Invalid window handle. public static implicit operator DpiOf(wnd w) => new(w); /// /// Invalid window handle. public static implicit operator DpiOf(System.Windows.Forms.Control c) => new(c); /// /// Invalid window handle. public static implicit operator DpiOf(System.Windows.DependencyObject c) => new(c); /// public static implicit operator DpiOf(IntPtr hMonitor) => new(hMonitor); /// public static implicit operator DpiOf(POINT screenOf) => new(screenOf); /// public static implicit operator DpiOf(RECT screenOf) => new(screenOf); /// public int Dpi => _dpi; /// public static implicit operator int(DpiOf d) => d._dpi; } } ================================================ FILE: Au/Au.More/FastBuffer.cs ================================================ namespace Au.More { /// /// Memory buffer on stack with ability to expand and use heap memory. /// Can be used for calling Windows API or building arrays. /// Must be used with [SkipLocalsInit] attribute; add it to the caller function or class. /// /// /// b = new(); /// for (; ; ) if (b.GetString(_GetEnvironmentVariable(name, b.p, b.n), out var s)) return s; /// } /// } /// ]]> /// [StructLayout(LayoutKind.Sequential, Size = 16 + StackSize + 16)] //16 for other fields + 16 for safety public unsafe ref struct FastBuffer where T : unmanaged { T* _p; //buffer pointer (on stack or native heap) int _n; //buffer length (count of T elements) bool _free; //if false, buffer is on stack in this variable (_p=&_stack). If true, buffer is allocated with MemoryUtil.Alloc. long _stack; //start of buffer of StackSize size /// /// A variable contains a field of this size. It is a memory buffer on stack. /// It is a byte count and does not depend on T. To get count of T elements on stack: StackSize/sizeof(T). /// public const int StackSize = 2048; //const int StackSize = 16; //test More() and GetString() //Also tested: // 1. Struct of normal size, when caller passes stackalloc'ed Span. Slow in Debug. And more caling code. // 2. See the commented out Buffer_. // 3. Callback. Good: easy to use, less calling code, don't need [SkipLocalsInit]. Bad: problem with captured variables (garbage, slow); slower in any case. // 4. foreach. Nothing good. /// /// Memory buffer pointer. /// public T* p => _p; /// /// Returns memory buffer pointer (). /// public static implicit operator T*(in FastBuffer b) => b._p; /// /// Gets reference to p[i]. Does not check bounds. /// public ref T this[int i] => ref _p[i]; /// /// Memory buffer length as number of elements of type T. /// public int n => _n; /// /// Allocates first buffer of default size. It is on stack (in this variable), and its length is StackSize/sizeof(T) elements of type T (2048 bytes or 1024 chars or 512 ints...). /// public FastBuffer() { //With this overload slightly faster. Also, the int overload is confusing when need buffer of default size. _stack = 0; fixed (long* t = &_stack) { _p = (T*)t; } //_p = (T*)Unsafe.AsPointer(ref _stack); //slower in Debug, same speed in Release _n = StackSize / sizeof(T); _free = false; } /// /// Allocates first buffer of specified size. /// /// /// Buffer length (number of elements of type T). /// If <= StackSize/sizeof(T), the buffer contains StackSize/sizeof(T) elements on stack (in this variable); it is 2048 bytes or 1024 chars or 512 ints... Else allocates native memory (much slower). /// public FastBuffer(int n) { _stack = 0; int nStack = StackSize / sizeof(T); if (_free = n > nStack) { _p = MemoryUtil.Alloc(n + 1); //+1 for safety _n = n; } else { _n = nStack; fixed (long* t = &_stack) { _p = (T*)t; } //_p = (T*)Unsafe.AsPointer(ref _stack); } //rejected: for medium-size buffers use ArrayPool. // It is usually much faster than MemoryUtil, but getting pinned pointer from array is slow. // To get pinned pointer, I know 3 ways. // 1. fixed(...){...}. Here cannot be used. // 2. GCHandle. Makes 3 times slower than just ArrayPool Rent/Return. Then MemoryUtil is only 50% slower. With this buffer size it does not matter. // 3. Memory/MemoryHandle. Same speed as GCHandle. MemoryHandle is bigger and managed. // Tested: MemoryPool is slower than ArrayPool and creates garbage. //tested: heap memory allocation becomes much slower starting from 1 MB. Then virtual memory is several times faster (else much slower). But with this buffer size it does not matter. } /// /// Allocates new bigger buffer of specified length. Frees old buffer if need. /// /// Number of elements of type T. /// Copy previous buffer contents to the new buffer. /// n <= current buffer length. public void More(int n, bool preserve = false) { if (_n == 0) throw new ArgumentNullException(null, "No buffer. Use another constructor."); //with many API would still work, but very slow if (n <= _n) throw new ArgumentException("n <= this.n"); if (!preserve) { Dispose(); _p = MemoryUtil.Alloc(n + 1); //+1 for safety } else if (_free) { MemoryUtil.ReAlloc(ref _p, n + 1); } else { var p = MemoryUtil.Alloc(n + 1); MemoryUtil.Copy(_p, p, _n * sizeof(T)); _p = p; } _n = n; _free = true; } /// /// Allocates new bigger buffer of at least n*2 length. Frees old buffer if need. /// /// Copy previous buffer contents to the new buffer. public void More(bool preserve = false) => More(Math.Max(checked(_n * 2), 0x4000 / sizeof(T)), preserve); //16 KB = StackSize * 8 /// /// Frees allocated memory if need. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Dispose() { if (_free) { MemoryUtil.Free(_p); _p = null; _n = 0; } } /// /// Gets API result as string, or allocates bigger buffer if old buffer was too small. /// This function can be used when T is char. /// /// The return value of the called Windows API function, if it returns string length or required buffer length. Or you can call . /// Receives the result string if succeeded, else sDefault (default null). /// /// Use if the API function isn't like this: ///
• If succeeds, returns string length without the terminating '\0' character. ///
• If buffer too small, returns required buffer length. ///
• If fails, returns 0. /// /// Set s = this string if buffer too small or r < 1 or if the retrieved string == this string (avoid creating new string). /// /// If r > , calls More(r); and returns false. /// Else creates new string of r length and returns true. /// public bool GetString(int r, out string s, BSFlags flags = 0, string sDefault = null) { if (sizeof(T) != 2) throw new InvalidOperationException(); //cannot use extension method that would be added only to FastBuffer. See GetString2 comments below. s = sDefault; if (r >= _n - 1) { if (0 != (flags & BSFlags.Truncates)) { if (r >= (0 != (flags & BSFlags.ReturnsLengthWith0) ? _n : _n - 1)) { More(); return false; } } else if (r > _n) { More(r); return false; } } if (r > 0) { if (0 != (flags & BSFlags.ReturnsLengthWith0)) r--; if (sDefault == null || !new Span(_p, r).SequenceEqual(sDefault)) s = new string((char*)_p, 0, r); } return true; } /// /// Finds length of '\0'-terminated UTF-16 string in buffer and converts to C# string. /// This function can be used when T is char. Use when length is unknown. /// /// /// If there is no '\0' character, gets whole buffer, and the string probably is truncated. /// public string GetStringFindLength() { return new((char*)_p, 0, FindStringLength()); } /// /// Finds length of '\0'-terminated char string in buffer. /// Returns if there is no '\0' character. /// public int FindStringLength() { if (sizeof(T) != 2) throw new InvalidOperationException(); return Ptr_.Length((char*)_p, _n); } /// /// Finds length of '\0'-terminated byte string in buffer. /// Returns if there is no '\0' character. /// public int FindByteStringLength() { if (sizeof(T) != 1) throw new InvalidOperationException(); return Ptr_.Length((byte*)_p, _n); } } } namespace Au.Types { //error CS1657: Cannot use 'b' as a ref or out value because it is a 'using variable'. //If in, compiles, but very slow. Probably copies t because calls More() which isn't readonly. //public static partial class ExtAu //{ // public static unsafe bool GetString2(this ref FastBuffer t, int r, out string s, BSFlags flags = 0, string sDefault = null) { // ... // } //} /// /// Flags for . /// [Flags] public enum BSFlags { /// /// If buffer too small, the API gets part of string instead of returning required buffer length. /// Truncates = 1, /// /// The API returns string length including the terminating '\0' character. /// ReturnsLengthWith0 = 2, } } //rejected. Maybe 5% faster, but not so easy to use. Need more code, and easy to forget something. Also VS warning if: for(var p = stackalloc ...). ///// ///// Allocates and frees native memory buffers for calling Windows API and other functions. ///// Used when need to retry when the primary stackalloc'ed buffer was too small. ///// ///// ///// b = new(); int n, r; ///// for (var p = stackalloc char[n = 1024]; ; p = b.Alloc(n = r)) { ///// r = api.GetCurrentDirectory(n, p); ///// if(r < n) return r > 0 ? new(p, 0, r) : null; ///// } ///// } ///// ///// [SkipLocalsInit] ///// unsafe static string WinText(wnd w) { ///// using Buffer_ b = new(); int n, r; ///// for (var p = stackalloc char[n = 1024]; ; p = b.Alloc(checked(n *= 2))) { ///// r = api.InternalGetWindowText(w, p, n); ///// if (r < n - 1) return new string(p, 0, r); ///// } ///// } ///// ]]> ///// //unsafe ref struct Buffer_ where T : unmanaged //{ // T* _p; // /// // /// Allocates n elements of type T of native memory. Frees old memory. // /// // public T* Alloc(int n) { // if (_p != null) { var p = _p; _p = null; MemoryUtil.Free(p); } // return _p = MemoryUtil.Alloc(n); // } // /// // /// Frees allocated memory. // /// // public void Dispose() { // if (_p != null) { var p = _p; _p = null; MemoryUtil.Free(p); } // } //} ================================================ FILE: Au/Au.More/FileOpenSaveDialog.cs ================================================ namespace Au.More { /// /// Shows standard "Open", "Save As" or "Select Folder" dialog to select a file or folder. /// /// /// This class exists because the similar .NET classes have these problems: /// - May disable the event. /// - As owner window they support only Window or Form. This class also supports window handles. /// - They support only filesystem items. /// - There is no option to not add the selected file to recent files that are displayed in the context menu of the taskbar button etc. /// - The WPF class does not have a client GUID property. /// /// There are 2 main dialog types - open () and save (). All other functions of this class (properties, etc) are common to opening and saving. /// /// /// /// public class FileOpenSaveDialog { readonly string _clientGuid; readonly bool _clearClientData; /// A GUID used to save the recently used folder path and other data of this dialog. Optional. See IFileDialog.SetClientGuid. /// Clear the recently used folder path and other data of this dialog. public FileOpenSaveDialog(string clientGuid = null, bool clearClientData = false) { _clientGuid = clientGuid; _clearClientData = clearClientData; } /// /// Options common to all dialog types. Rarely used. /// public FOSFlags CommonFlags { get; set; } /// /// List of file types, like with . /// Example: "Text files|*.txt|Office files|*.doc;*.xls|All files|*.*" /// public Strings FileTypes { get; set; } /// /// 1-based index of the selected file type (see ). /// public int FileTypeIndex { get; set; } /// /// Default extension to add to file names. /// Must be like "txt", not like ".txt". /// If null (default), gets from ; set "" to prevent it. /// public string DefaultExt { get; set; } /// /// Initial folder for the first time. Later is used the recently used folder instead. /// public string InitFolderFirstTime { get; set; } /// /// Initial folder to use now. /// In most cases it's recommended to use instead. /// /// /// /// public string InitFolderNow { get; set; } /// /// Sets the initial file name text in the edit box. /// public string FileNameText { private get; set; } /// /// Sets the file name edit box label. /// public string FileNameLabel { private get; set; } /// /// Sets the OK button label. /// public string OkButtonLabel { private get; set; } /// /// Sets the dialog window name. /// public string Title { private get; set; } object _Show(bool save, _Api.FOS f, AnyWnd owner, string saveFile = null) { var w = owner.Hwnd; if (w.Is0) { w = wnd.active; if (!w.IsOfThisThread) w = default; } var d = (_Api.IFileDialog)Activator.CreateInstance(Type.GetTypeFromCLSID(save ? _Api.CLSID_FileSaveDialog : _Api.CLSID_FileOpenDialog)); if (_clientGuid != null) d.SetClientGuid(new(_clientGuid)); if (_clearClientData) d.ClearClientData(); f |= (_Api.FOS)CommonFlags ^ (_Api.FOS.FOS_DONTADDTORECENT | _Api.FOS.FOS_PATHMUSTEXIST); //print.it(f); d.SetOptions(f); var defExt = DefaultExt; var ft = FileTypes.Value == null ? null : FileTypes.ToArray(); if (ft != null) { int n = ft.Length / 2, i = FileTypeIndex; d.SetFileTypes(n, ft); if (i > 0) d.SetFileTypeIndex(i--); if (defExt == null && (uint)i < n) ft[i * 2 + 1].RxMatch(@"^\*\.([^*?;]+)", 1, out defExt); } if (!defExt.NE()) d.SetDefaultExtension(defExt); if (InitFolderFirstTime != null && _ShellItemFromString(InitFolderFirstTime, out var si1)) d.SetDefaultFolder(si1); if (InitFolderNow != null && _ShellItemFromString(InitFolderNow, out var si2)) d.SetFolder(si2); if (FileNameText != null) d.SetFileName(FileNameText); if (FileNameLabel != null) d.SetFileNameLabel(FileNameLabel); if (OkButtonLabel != null) d.SetOkButtonLabel(OkButtonLabel); if (Title != null) d.SetTitle(Title); if (saveFile != null && d is _Api.IFileSaveDialog sd && _ShellItemFromString(saveFile, out var si3)) sd.SetSaveAsItem(si3); //if (0!=d.Show(w)) return null; //API bug: after d.Show stops working AppDomain.UnhandledException event. // Same with .NET wrappers; same when the native API called in C++; same with API GetOpenFileName. // The API removes the .NET's unhandled exception filter and sets null filter. Need to restore it. // AppModuleInit_ auto-restores, but in some apps it isn't called. // Fixed in Win11. var uef = Api.SetUnhandledExceptionFilter(0); Api.SetUnhandledExceptionFilter(uef); try { if (0 != d.Show(w)) return null; } finally { Api.SetUnhandledExceptionFilter(uef); } if (ft != null) FileTypeIndex = d.GetFileTypeIndex(); //FileNameText=d.GetFileName(); //fails here. And not useful. if (!f.Has(_Api.FOS.FOS_ALLOWMULTISELECT)) return _ShellItemToString(d.GetResult()); var r = ((_Api.IFileOpenDialog)d).GetResults(); var a = new string[r.GetCount()]; for (int i = 0; i < a.Length; i++) a[i] = _ShellItemToString(r.GetItemAt(i)); return a; } object _ShowOpen(AnyWnd owner = default, bool multiSelect = false, bool selectFolder = false, bool onlyFilesystem = true, bool fileMustExist = true, bool previewPane = false) { //default FOS_NOCHANGEDIR, FOS_PATHMUSTEXIST, FOS_FILEMUSTEXIST var f = _Api.FOS.FOS_NOCHANGEDIR; if (multiSelect) f |= _Api.FOS.FOS_ALLOWMULTISELECT; if (selectFolder) f |= _Api.FOS.FOS_PICKFOLDERS; if (onlyFilesystem) f |= _Api.FOS.FOS_FORCEFILESYSTEM; else f |= _Api.FOS.FOS_ALLNONSTORAGEITEMS; if (fileMustExist) f |= _Api.FOS.FOS_FILEMUSTEXIST; if (previewPane) f |= _Api.FOS.FOS_FORCEPREVIEWPANEON; return _Show(false, f, owner); } /// /// Shows "Open" or "Select Folder" dialog that allows to select single item. /// /// Full path of the selected file. /// Owner window. Optional. /// Select folders, not files. /// The dialog allows to select only file system items (files, folders), not other shell items or URLs. Default true. If false, other shell items are returned like ":: ITEMIDLIST"; see . /// The dialog can return only existing items. Default true. /// Display the preview pane. /// true on OK, false on Cancel or error. public bool ShowOpen(out string result, AnyWnd owner = default, bool selectFolder = false, bool onlyFilesystem = true, bool fileMustExist = true, bool previewPane = false) { result = _ShowOpen(owner, false, selectFolder, onlyFilesystem, fileMustExist, previewPane) as string; return result != null; } /// /// Shows "Open" or "Select Folder" dialog that allows to select multiple items. /// /// Full paths of the selected files. /// public bool ShowOpen(out string[] result, AnyWnd owner = default, bool selectFolder = false, bool onlyFilesystem = true, bool fileMustExist = true, bool previewPane = false) { result = _ShowOpen(owner, true, selectFolder, onlyFilesystem, fileMustExist, previewPane) as string[]; return result != null; } /// /// Shows "Save As" dialog. /// /// Full path of the selected file. /// Owner window. Optional. /// If the selected file already exists, show a message box. Default true. /// The initially selected file. Its name is displayed in the file name edit box, and the containing folder is opened. This would generally be used when the application is saving a file that already exists. For new files use . /// true on OK, false on Cancel or error. public bool ShowSave(out string result, AnyWnd owner = default, bool overwritePrompt = true, string initFile = null) { //default FOS_OVERWRITEPROMPT, FOS_NOCHANGEDIR, FOS_PATHMUSTEXIST, FOS_NOREADONLYRETURN var f = _Api.FOS.FOS_NOCHANGEDIR; if (overwritePrompt) f |= _Api.FOS.FOS_OVERWRITEPROMPT; /*if(!overwriteReadonly)*/ f |= _Api.FOS.FOS_NOREADONLYRETURN; //always works like with this flag //if(createPrompt) f|=_Api.FOS.FOS_CREATEPROMPT; //does not work. The .NET wrapper shows messagebox explicitly. //if(!testCreate) f|=_Api.FOS.FOS_NOTESTFILECREATE; //not important, not tested //if(strictFileTypes) f|=_Api.FOS.FOS_STRICTFILETYPES; //does not work result = _Show(true, f, owner, initFile) as string; return result != null; } static string _ShellItemToString(_Api.IShellItem r) { //if(!f.Has(_Api.FOS.FOS_FORCEFILESYSTEM)) { // var k=r.GetAttributes(0xffffffff); // if(0==(k&_Api.SFGAO_FILESYSTEM)) { // print.it(k); // } //} var s = r.GetDisplayName(SIGDN.FILESYSPATH | SIGDN.URL); //info: for a non-FS item, even with SIGDN.FILESYSPATH gets string like "::{GUID}" return Pidl.ClsidToItemidlist_(s); } static bool _ShellItemFromString(string path, out _Api.IShellItem si) { if (path.Starts(":: ")) { var p = Pidl.FromString(path); if (p == null) { si = null; return false; } return 0 == _Api.SHCreateItemFromIDList(p.UnsafePtr, typeof(_Api.IShellItem).GUID, out si); } else { return 0 == _Api.SHCreateItemFromParsingName(path, default, typeof(_Api.IShellItem).GUID, out si); } } unsafe class _Api { internal static Guid CLSID_FileOpenDialog = new(0xDC1C5A9C, 0xE88A, 0x4DDE, 0xA5, 0xA1, 0x60, 0xF8, 0x2A, 0x20, 0xAE, 0xF7); internal static Guid CLSID_FileSaveDialog = new(0xC0B4E2F3, 0xBA21, 0x4773, 0x8D, 0xBA, 0x33, 0x5E, 0xC9, 0x46, 0xEB, 0x8B); [ComImport, Guid("d57c7288-d4ad-4768-be02-9d969532d960"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IFileOpenDialog : IFileDialog { //IModalWindow [PreserveSig] new int Show(wnd hwndOwner); //IFileDialog new void SetFileTypes(int cFileTypes, [MarshalAs(UnmanagedType.LPArray)][In] string[] rgFilterSpec); new void SetFileTypeIndex(int iFileType); new int GetFileTypeIndex(); [PreserveSig] new int Advise(/*IFileDialogEvents pfde, out uint pdwCookie*/); [PreserveSig] new int Unadvise(uint dwCookie); new void SetOptions(_Api.FOS fos); new _Api.FOS GetOptions(); new void SetDefaultFolder(IShellItem psi); new void SetFolder(IShellItem psi); new IShellItem GetFolder(); new IShellItem GetCurrentSelection(); new void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName); [return: MarshalAs(UnmanagedType.LPWStr)] new string GetFileName(); new void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle); new void SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string pszText); new void SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string pszLabel); new IShellItem GetResult(); [PreserveSig] new int AddPlace(/*IShellItem psi, FDAP fdap*/); new void SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); [PreserveSig] new int Close(int hr); new void SetClientGuid(in Guid guid); new void ClearClientData(); [PreserveSig] new int SetFilter(/*IShellItemFilter pFilter*/); //IFileOpenDialog IShellItemArray GetResults(); IShellItemArray GetSelectedItems(); } [ComImport, Guid("84bccd23-5fde-4cdb-aea4-af64b83d78ab"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IFileSaveDialog : IFileDialog { //IModalWindow [PreserveSig] new int Show(wnd hwndOwner); //IFileDialog new void SetFileTypes(int cFileTypes, [MarshalAs(UnmanagedType.LPArray)][In] string[] rgFilterSpec); new void SetFileTypeIndex(int iFileType); new int GetFileTypeIndex(); [PreserveSig] new int Advise(/*IFileDialogEvents pfde, out uint pdwCookie*/); [PreserveSig] new int Unadvise(uint dwCookie); new void SetOptions(_Api.FOS fos); new _Api.FOS GetOptions(); new void SetDefaultFolder(IShellItem psi); new void SetFolder(IShellItem psi); new IShellItem GetFolder(); new IShellItem GetCurrentSelection(); new void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName); [return: MarshalAs(UnmanagedType.LPWStr)] new string GetFileName(); new void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle); new void SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string pszText); new void SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string pszLabel); new IShellItem GetResult(); [PreserveSig] new int AddPlace(/*IShellItem psi, FDAP fdap*/); new void SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); [PreserveSig] new int Close(int hr); new void SetClientGuid(in Guid guid); new void ClearClientData(); [PreserveSig] new int SetFilter(/*IShellItemFilter pFilter*/); //IFileSaveDialog void SetSaveAsItem(IShellItem psi); [PreserveSig] int SetProperties(/*IPropertyStore pStore*/); [PreserveSig] int SetCollectedProperties(/*IPropertyDescriptionList pList, [MarshalAs(UnmanagedType.Bool)] bool fAppendDefault*/); [PreserveSig] int GetProperties(/*out IPropertyStore ppStore*/); [PreserveSig] int ApplyProperties(/*IShellItem psi, IPropertyStore pStore, wnd hwnd, IFileOperationProgressSink pSink*/); } [ComImport, Guid("42f85136-db7e-439c-85f1-e4075d135fc8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IFileDialog { //IModalWindow [PreserveSig] int Show(wnd hwndOwner); //IFileDialog void SetFileTypes(int cFileTypes, [MarshalAs(UnmanagedType.LPArray)][In] string[] rgFilterSpec); void SetFileTypeIndex(int iFileType); int GetFileTypeIndex(); [PreserveSig] int Advise(/*IFileDialogEvents pfde, out uint pdwCookie*/); [PreserveSig] int Unadvise(uint dwCookie); void SetOptions(_Api.FOS fos); _Api.FOS GetOptions(); void SetDefaultFolder(IShellItem psi); void SetFolder(IShellItem psi); IShellItem GetFolder(); IShellItem GetCurrentSelection(); void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName); [return: MarshalAs(UnmanagedType.LPWStr)] string GetFileName(); void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle); void SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string pszText); void SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string pszLabel); IShellItem GetResult(); [PreserveSig] int AddPlace(/*IShellItem psi, FDAP fdap*/); void SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); [PreserveSig] int Close(int hr); void SetClientGuid(in Guid guid); void ClearClientData(); [PreserveSig] int SetFilter(/*IShellItemFilter pFilter*/); } [ComImport, Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IShellItem { [PreserveSig] int BindToHandler(IntPtr pbc, in Guid bhid, in Guid riid, void** ppv); [PreserveSig] int GetParent(out IShellItem ppsi); [return: MarshalAs(UnmanagedType.LPWStr)] string GetDisplayName(SIGDN sigdnName); uint GetAttributes(uint sfgaoMask); [PreserveSig] int Compare(IShellItem psi, uint hint, out int piOrder); } [ComImport, Guid("b63ea76d-1f85-456f-a19c-48159efa858b"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IShellItemArray { [PreserveSig] int BindToHandler(/*IntPtr pbc, in Guid bhid, in Guid riid, void** ppvOut*/); [PreserveSig] int GetPropertyStore(/*GETPROPERTYSTOREFLAGS flags, in Guid riid, void** ppv*/); [PreserveSig] int GetPropertyDescriptionList(/*in PROPERTYKEY keyType, in Guid riid, void** ppv*/); [PreserveSig] int GetAttributes(/*SIATTRIBFLAGS AttribFlags, uint sfgaoMask, out uint psfgaoAttribs*/); int GetCount(); IShellItem GetItemAt(int dwIndex); [PreserveSig] int EnumItems(/*out IEnumShellItems ppenumShellItems*/); } [Flags] internal enum FOS : uint { FOS_OVERWRITEPROMPT = 0x2, FOS_STRICTFILETYPES = 0x4, FOS_NOCHANGEDIR = 0x8, FOS_PICKFOLDERS = 0x20, FOS_FORCEFILESYSTEM = 0x40, FOS_ALLNONSTORAGEITEMS = 0x80, FOS_NOVALIDATE = 0x100, FOS_ALLOWMULTISELECT = 0x200, FOS_PATHMUSTEXIST = 0x800, FOS_FILEMUSTEXIST = 0x1000, FOS_CREATEPROMPT = 0x2000, FOS_SHAREAWARE = 0x4000, FOS_NOREADONLYRETURN = 0x8000, FOS_NOTESTFILECREATE = 0x10000, FOS_HIDEMRUPLACES = 0x20000, FOS_HIDEPINNEDPLACES = 0x40000, FOS_NODEREFERENCELINKS = 0x100000, FOS_OKBUTTONNEEDSINTERACTION = 0x200000, FOS_DONTADDTORECENT = 0x2000000, FOS_FORCESHOWHIDDEN = 0x10000000, FOS_DEFAULTNOMINIMODE = 0x20000000, FOS_FORCEPREVIEWPANEON = 0x40000000, FOS_SUPPORTSTREAMABLEITEMS = 0x80000000 } [DllImport("shell32.dll", PreserveSig = true)] internal static extern int SHCreateItemFromParsingName(string pszPath, IntPtr pbc, in Guid riid, out IShellItem ppv); [DllImport("shell32.dll", PreserveSig = true)] internal static extern int SHCreateItemFromIDList(IntPtr pidl, in Guid riid, out IShellItem ppv); } } } namespace Au.Types { /// /// . /// [Flags] public enum FOSFlags : uint //the values are like in FOS, some xored { /// Add the selected file to recent documents. See API SHAddToRecentDocs. AddToRecent = 0x2000000, /// Do not check for situations that would prevent an application from opening the selected file, such as sharing violations or access denied errors. NoValidateAccess = 0x100, ///// The user can enter a path that does not exist, like C:\does not exist\file.txt. //NoValidatePath = 0x800, //does not work. Always validates. /// Shortcuts should not be treated as their target items, allowing an application to open a .lnk file. NoDereferenceLinks = 0x100000, /// Show hidden and system items. ShowHidden = 0x10000000, } } ================================================ FILE: Au/Au.More/GdiTextRenderer.cs ================================================ //FUTURE: support columns. Maybe use \t of specified widths. namespace Au.More; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member /// /// Draws text using fastest GDI API such as TextOut and standard UI font. /// Can easily draw string parts with different colors/styles without measuring. /// Must be disposed. /// public unsafe class GdiTextRenderer : IDisposable { IntPtr _dc, _oldFont; uint _oldAlign; int _color, _oldColor; int _dpi; bool _releaseDC; /// Object created with this ctor can draw and measure. /// Device context handle. Dispose will not release it. /// public GdiTextRenderer(IntPtr hdc, int dpi) { _dpi = dpi; _dc = hdc; _oldFont = Api.SelectObject(_dc, NativeFont_.RegularCached(_dpi)); _oldAlign = 0xffffffff; Api.SetBkMode(_dc, 1); _oldColor = Api.SetTextColor(_dc, _color = 0); } /// Object created with this ctor can measure only. Uses screen DC. public GdiTextRenderer(int dpi) { _dpi = dpi; _releaseDC = true; _dc = Api.GetDC(default); _oldFont = Api.SelectObject(_dc, NativeFont_.RegularCached(_dpi)); } public void Dispose() { if (_dc != default) { Api.SelectObject(_dc, _oldFont); if (_releaseDC) Api.ReleaseDC(default, _dc); else { if (_oldAlign != 0xffffffff) Api.SetTextAlign(_dc, _oldAlign); if (_oldColor != _color) Api.SetTextColor(_dc, _oldColor); } _dc = default; } //never mind: we don't restore the old current position. Nobody later would draw at current position without movetoex. As well as bkmode. } /// /// Sets the current drawing position of the DC. /// Returns previous position. /// public POINT MoveTo(int x, int y) { Api.MoveToEx(_dc, x, y, out var p); return p; } /// /// Gets the current drawing position of the DC. /// public POINT GetCurrentPosition() { Api.GetCurrentPositionEx(_dc, out var p); return p; } /// /// Sets non-bold font. /// public void FontNormal() => Api.SelectObject(_dc, NativeFont_.RegularCached(_dpi)); /// /// Sets bold font. /// public void FontBold() => Api.SelectObject(_dc, NativeFont_.BoldCached(_dpi)); //public void FontItalic() => Api.SelectObject(_dc, NativeFont_.ItalicCached(_dpi)); //public void FontBoldItalic() => Api.SelectObject(_dc, NativeFont_.BoldItalicCached(_dpi)); /// /// Draws text at the current drawing position of the DC, and updates it. /// /// Text color 0xBBGGRR. /// Background color 0xBBGGRR. Transparent if null. public void DrawText(string s, int color = 0, Range? range = null, int? backColor = null) { var (from, len) = range.GetOffsetAndLength(s.Lenn()); if (len == 0) return; if (_oldAlign == 0xffffffff) _oldAlign = Api.SetTextAlign(_dc, 1); //TA_UPDATECP _DrawText(s, 0, 0, color, from, len, backColor); } /// /// Draws text at specified position. Does not use/update the current drawing position of the DC. /// /// Text color 0xBBGGRR. /// Background color 0xBBGGRR. Transparent if null. public void DrawText(string s, POINT p, int color = 0, Range? range = null, int? backColor = null) { var (from, len) = range.GetOffsetAndLength(s.Lenn()); if (len == 0) return; if (_oldAlign != 0xffffffff) { Api.SetTextAlign(_dc, _oldAlign); _oldAlign = 0xffffffff; } _DrawText(s, p.x, p.y, color, from, len, backColor); } /// /// Draws text clipped in specified rectangle. Does not use/update the current drawing position of the DC. /// /// Text color 0xBBGGRR. /// Background color 0xBBGGRR. Transparent if null. public void DrawText(string s, in RECT r, int color = 0, Range? range = null, int? backColor = null) { var (from, len) = range.GetOffsetAndLength(s.Lenn()); if (len == 0) return; if (_oldAlign != 0xffffffff) { Api.SetTextAlign(_dc, _oldAlign); _oldAlign = 0xffffffff; } _DrawText(s, r, color, from, len, backColor); } //[Ext]TextOut fails if >= 1024 * 64. // On Win7 and 8.1 depends on text width (ushort.MaxValue?). Eg fails if > ~2900 'W'. // ExtTextOut doc: "This value may not exceed 8192.". static int _LimitLength(int len) => Math.Min(len, osVersion.minWin10 ? 1024 * 64 - 1 : 4000); unsafe void _DrawText(string s, int x, int y, int color, int from, int len, int? backColor) { if (color != _color) Api.SetTextColor(_dc, _color = color); using var bc = new _BackColor(_dc, backColor); fixed (char* p = s) Api.TextOut(_dc, x, y, p + from, _LimitLength(len)); } unsafe void _DrawText(string s, in RECT r, int color, int from, int len, int? backColor) { if (color != _color) Api.SetTextColor(_dc, _color = color); using var bc = new _BackColor(_dc, backColor); fixed (char* p = s) Api.ExtTextOut(_dc, r.left, r.top, Api.ETO_CLIPPED, r, p + from, _LimitLength(len)); } public SIZE MeasureText(string s, Range? range = null) { var (from, len) = range.GetOffsetAndLength(s.Lenn()); if (len == 0) return default; fixed (char* p = s) { Api.GetTextExtentPoint32(_dc, p + from, _LimitLength(len), out var z); return z; } } ref struct _BackColor { int _oldColor, _oldMode; IntPtr _dc; public _BackColor(IntPtr dc, int? color) { if (color != null) { _dc = dc; _oldMode = Api.SetBkMode(_dc, 2); _oldColor = Api.SetBkColor(_dc, color.Value); } } public void Dispose() { if (_dc != default) { Api.SetBkColor(_dc, _oldColor); Api.SetBkMode(_dc, _oldMode); } } } } ================================================ FILE: Au/Au.More/Hash.cs ================================================ using System.Security.Cryptography; using System.Buffers.Text; namespace Au.More; /// /// Data hash functions. /// [EditorBrowsable(EditorBrowsableState.Never)] //obsolete as public, but still used internally. Created before the easy and fast .NET hash classes existed. public static unsafe class Hash { #region FNV1 /// /// 32-bit FNV-1 hash. /// Useful for fast hash table and checksum use, not cryptography. Similar to CRC32; faster but creates more collisions. /// public static int Fnv1(RStr data) { if (data.IsNull()) return 0; uint hash = 2166136261; for (int i = 0; i < data.Length; i++) hash = (hash * 16777619) ^ data[i]; return (int)hash; } /// public static int Fnv1(char* data, int lengthChars) => Fnv1(new RStr(data, lengthChars)); /// Data. See also: , . /// public static int Fnv1(RByte data) { if (data.IsNull()) return 0; uint hash = 2166136261; for (int i = 0; i < data.Length; i++) hash = (hash * 16777619) ^ data[i]; return (int)hash; } /// public static int Fnv1(byte* data, int lengthBytes) => Fnv1(new RByte(data, lengthBytes)); /// public static int Fnv1(T data) where T : unmanaged => Fnv1((byte*)&data, sizeof(T)); /// /// 64-bit FNV-1 hash. /// public static long Fnv1Long(RStr data) { if (data.IsNull()) return 0; ulong hash = 14695981039346656037; for (int i = 0; i < data.Length; i++) hash = (hash * 1099511628211) ^ data[i]; return (long)hash; } /// /// 64-bit FNV-1 hash. /// public static long Fnv1Long(char* data, int lengthChars) => Fnv1(new RStr(data, lengthChars)); /// /// 64-bit FNV-1 hash. /// /// Data. See also: , . public static long Fnv1Long(RByte data) { if (data.IsNull()) return 0; ulong hash = 14695981039346656037; for (int i = 0; i < data.Length; i++) hash = (hash * 1099511628211) ^ data[i]; return (long)hash; } /// /// 64-bit FNV-1 hash. /// public static long Fnv1Long(byte* data, int lengthBytes) => Fnv1(new RByte(data, lengthBytes)); /// /// 64-bit FNV-1 hash. /// public static long Fnv1Long(T data) where T : unmanaged => Fnv1Long((byte*)&data, sizeof(T)); /// /// FNV-1 hash, modified to make faster with long strings (then takes every n-th character). /// [Obsolete] //unused public static int Fast(char* data, int lengthChars) { if (data == null) return 0; //Also we take the last 1-2 characters (in the second loop), because often there are several strings like Chrome_WidgetWin_0, Chrome_WidgetWin_1... //Also we hash uints, not chars, unless the string is very short. //Tested speed with 400 unique strings (window/control names/classnames/programnames). The time was 7 mcs. For single call 17 ns. uint hash = 2166136261; int i = 0; if (lengthChars > 8) { int lc = lengthChars--; lengthChars /= 2; //we'll has uints, not chars int every = lengthChars / 8 + 1; for (; i < lengthChars; i += every) hash = (hash * 16777619) ^ ((uint*)data)[i]; i = lengthChars * 2; lengthChars = lc; } for (; i < lengthChars; i++) hash = (hash * 16777619) ^ data[i]; return (int)hash; } /// /// FNV-1 hash, modified to make faster with long strings (then takes every n-th character). /// /// String. Can be null. [Obsolete] //unused public static int Fast(RStr s) { fixed (char* p = s) return Fast(p, s.Length); } #endregion #region MD5 /// /// Computes MD5 hash of data. /// Call an method one or more times. Finally use to get result. /// [StructLayout(LayoutKind.Explicit)] public struct MD5Context { //MD5_CTX + _state [FieldOffset(88)] MD5Result _result; [FieldOffset(104)] long _state; //1 inited/added, 2 finalled /// /// true if no data was added. /// public bool IsEmpty => _state == 0; /// Adds data. /// size < 0. /// data is null and size > 0. public void Add(void* data, int size) { if (size < 0) throw new ArgumentOutOfRangeException(); if (size > 0) Not_.Null(data); //allow null if size 0. Eg 'fixed' gets null pointer if the span or array is empty. if (_state != 1) { Api.MD5Init(out this); _state = 1; } if (size > 0) Api.MD5Update(ref this, data, size); } /// Adds data. public void Add(T data) where T : unmanaged => Add(&data, sizeof(T)); /// Adds data. /// Data. See also: , . public void Add(RByte data) { fixed (byte* p = data) Add(p, data.Length); //note: p null if data empty } /// Adds string converted to UTF-8. /// data is null. public void Add(string data) => Add(Encoding.UTF8.GetBytes(data)); //CONSIDER: alloc on stack to avoid garbage. This func works, but not faster. //[SkipLocalsInit] //public void Add2(string data) { // if (data.Length < 3000) { // Span p = stackalloc byte[data.Length * 3]; // int n = Encoding.UTF8.GetBytes(data, p); // //print.it(n); // Add(p[..n]); // } else { // Add(Encoding.UTF8.GetBytes(data)); // } //} //rejected. Better use unsafe address, then will not need to copy data. ///// Adds data. //public void Add(T data) where T: unmanaged //{ // Add(&data, sizeof(T)); //} /// /// Computes final hash of datas added with an method. /// /// Add was not called. /// /// Resets state, so that if Add called again, it will start adding new datas. /// public MD5Result Hash { get { if (_state != 2) { if (_state != 1) throw new InvalidOperationException(); Api.MD5Final(ref this); _state = 2; } return _result; } } } /// /// Result of . /// It is 16 bytes stored in 2 long fields r1 and r2. /// If need, can be converted to byte[] with or to hex string with . /// public record struct MD5Result { #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member public readonly long r1, r2; public MD5Result(long r1, long r2) { this.r1 = r1; this.r2 = r2; } #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member /// /// Converts this to byte[] of length 16. /// public byte[] ToArray() { var r = new byte[16]; fixed (byte* p = r) { *(long*)p = r1; *(long*)(p + 8) = r2; } return r; } /// /// Converts this to hex string. /// public override string ToString() => Convert2.HexEncode(this); /// /// Creates from hex string returned by . /// /// false if encoded is invalid. public static bool FromString(RStr encoded, out MD5Result r) => Convert2.HexDecode(encoded, out r); #if NET9_0_OR_GREATER /// /// Converts this to Base64url string (URL-safe Base64). /// public string ToStringBase64Url() => Base64Url.EncodeToString(_AsSpan()); /// /// Creates from string returned by . /// public static bool FromStringBase64Url(RStr encoded, out MD5Result r) { r = default; return Base64Url.TryDecodeFromChars(encoded, r._AsSpan(), out _); } Span _AsSpan() => MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref this, 1)); #endif } /// /// Computes MD5 hash of data. /// Uses . /// /// Data. See also: , . public static MD5Result MD5(RByte data) { MD5Context md = default; md.Add(data); return md.Hash; } //rejected. Problems with overload resolution and implicit conversion Span to ReadOnlySpan. Not so often used. Then would need the same everywhere. Instead added doc. ///// ///// Computes MD5 hash of data. ///// Uses . ///// //public static MD5Result MD5(ReadOnlySpan data) where T : unmanaged // => MD5(MemoryMarshal.AsBytes(data)); /// /// Computes MD5 hash of string converted to UTF-8. /// Uses . /// public static MD5Result MD5(string data) { MD5Context md = default; md.Add(data); return md.Hash; } /// /// Computes MD5 hash of data. Returns result as hex or base64 string. /// Uses . /// /// Data. See also: , . /// public static string MD5(RByte data, bool base64) { var h = MD5(data); return base64 ? Convert.ToBase64String(new RByte((byte*)&h, 16)) : h.ToString(); } ///// ///// Computes MD5 hash of data. Returns result as hex or base64 string. ///// Uses . ///// //public static string MD5(ReadOnlySpan data, bool base64) where T : unmanaged // => MD5(MemoryMarshal.AsBytes(data), base64); /// /// Computes MD5 hash of string converted to UTF-8. Returns result as hex or base64 string. /// Uses . /// public static string MD5(string data, bool base64) { var h = MD5(data); return base64 ? Convert.ToBase64String(new RByte((byte*)&h, 16)) : h.ToString(); } #endregion #region other /// /// Computes data hash using the specified cryptographic algorithm. /// /// Data. See also: , . /// Algorithm name, eg "SHA256". See . public static byte[] Crypto(RByte data, string algorithm) { using var x = (HashAlgorithm)CryptoConfig.CreateFromName(algorithm); var r = new byte[x.HashSize / 8]; x.TryComputeHash(data, r, out _); return r; } /// /// Computes hash of string converted to UTF-8, using the specified cryptographic algorithm. /// /// /// Algorithm name, eg "SHA256". See . public static byte[] Crypto(string data, string algorithm) => Crypto(Encoding.UTF8.GetBytes(data), algorithm); /// /// Computes data hash using the specified cryptographic algorithm. Returns result as hex or base64 string. /// /// Data. See also: , . /// Algorithm name, eg "SHA256". See . /// public static string Crypto(RByte data, string algorithm, bool base64) { var b = Crypto(data, algorithm); return base64 ? Convert.ToBase64String(b) : Convert2.HexEncode(b); } /// /// Computes hash of string converted to UTF-8, using the specified cryptographic algorithm. Returns result as hex or base64 string. /// /// /// Algorithm name, eg "SHA256". See . /// public static string Crypto(string data, string algorithm, bool base64) { var b = Crypto(data, algorithm); return base64 ? Convert.ToBase64String(b) : Convert2.HexEncode(b); } #endregion } ================================================ FILE: Au/Au.More/HelpUtil.cs ================================================ namespace Au.More; /// /// Static functions to open a help topic etc. /// public static class HelpUtil { /// /// Opens a LibreAutomate help topic. /// /// Topic file name, like "wnd.find" or "Au.Types.RECT" or "articles/Wildcard expression". public static void AuHelp(string topic) { #pragma warning disable CS0612 //Type or member is obsolete var url = AuHelpUrl(topic); #pragma warning restore CS0612 //Type or member is obsolete if (AuHelpEvent_ is { } e) { var k = new AuHelpEventArgs_ { Url = url }; e(k); if (k.Cancel) return; url = k.Url; } run.itSafe(url); } /// /// Gets URL of a LibreAutomate help topic. /// /// Topic file name, like "wnd.find" or "Au.Types.RECT" or "articles/Wildcard expression". public static string AuHelpUrl(string topic) { string fragment = null; int i = topic.IndexOf('#'); if (i >= 0) { fragment = topic[i..]; topic = topic[..i]; } if (topic.Ends(".this[]")) topic = topic.ReplaceAt(^7.., ".Item"); else if (topic.Ends(".this")) topic = topic.ReplaceAt(^5.., ".Item"); else if (topic.Ends("[]")) topic = topic.ReplaceAt(^2.., ".Item"); else if (topic.Contains("timer")) topic = topic.RxReplace(@"\btimer2?\.(after|every)\b\K", "_1"); //the filename has this suffix because of the instance method After/Every var url = AuHelpBaseUrl; if (!url.Ends('/')) url += "/"; if (!topic.NE()) url = url + (topic.Contains('/') ? null : (topic.Starts("Au.") ? "api/" : "api/Au.")) + topic + (topic.Ends(".html") || topic.Ends('/') ? null : ".html") + fragment; return url; } /// /// s.Starts(false, "Au.", "articles/", "editor/", "cookbook/", "api/") > 0 /// internal static bool IsAuHelp_(string s) => s.Starts(false, "Au.", "articles/", "editor/", "cookbook/", "api/") > 0; /// /// URL of the LibreAutomate documentation website: "https://www.libreautomate.com/". /// public static string AuHelpBaseUrl => "https://www.libreautomate.com/"; internal static event Action AuHelpEvent_; internal class AuHelpEventArgs_ : CancelEventArgs { public string Url { get; set; } } } ================================================ FILE: Au/Au.More/HttpServerSession.cs ================================================ using System.Net; using System.Net.Sockets; using System.Text.Json; namespace Au.More { /// /// Simple HTTP 1.1 server. Can be used for communicating between two scripts or apps on local network, internet or same computer. Also can receive requests from web browsers etc. /// /// /// To receive HTTP messages, create a class with base HttpServerSession and add function . Example in . /// public abstract class HttpServerSession { /// /// Simple HTTP 1.1 server. /// /// TCP port. Default 4455. If 0, uses the first available port in the dynamic/private ports range (49152-65535); you can use the started callback to discover it. /// A local IPv4 or IPv6 address, like "127.0.0.1" or "[::1]". If null (default), uses and dual mode (supports IPv6 and IPv4 connections). /// Called when the server started (after ). /// Exceptions of functions. Unlikely. /// /// Runs all the time and listens for new TCP client connections. For each connected client starts new thread, creates new object of your -based type, and calls , which calls . Supports keep-alive. Multiple sessions can run simultaneously. /// /// Uses , not , therefore don't need administrator privileges, netsh and opening ports in firewall. Just the standard firewall dialog first time. /// /// The HTTP server is accessible from local network computers. Usually not accessible from the internet. To make accessible from the internet, you can use ngrok or similar software. This server does not support https (secure connections), but ngrok makes internet connections secure. /// /// The Windows Firewall shows a dialog when the app uses a HTTP server the first time, unless ip is "127.0.0.1". /// /// /// HTTP server. /// (); /// /// class MyHttpSession : HttpServerSession { /// //protected override void Run() { /// // print.it($"Session started. Client IP: {ClientIP}"); /// // base.Run(); /// // print.it("Session ended"); /// //} /// /// protected override void MessageReceived(HSMessage m, HSResponse r) { /// //Auth((u, p) => p == "password206474"); /// /// print.it(m.Method, m.TargetPath, m.UrlParameters, m.Headers, m.ContentText); /// /// string response = "RESPONSE"; /// r.SetContent(response); /// //or /// //r.Content = response.ToUTF8(); /// //r.Headers.Add("Content-Type", "text/plain; charset=utf-8"); /// } /// } /// ]]> /// HTTP client. /// /// public static void Listen(int port = 4455, string ip = null, Action started = null) where TSession : HttpServerSession, new() { var server = ip == null ? TcpListener.Create(port) //IPAddress.IPv6Any, dual mode (supports IPv6 and IPv4 connections); shows Firewall popup. : new(IPAddress.Parse(ip), port); //no Firewall popup if "127.0.0.1" or "[::1]", but then no dual mode; `server.Server.DualMode=true` fails. server.Start(); started?.Invoke(server); try { for (; ; ) { var client = server.AcceptTcpClient(); run.thread(() => { new TSession()._Run(client); Interlocked.Decrement(ref s_nThreads); }).Name = "Au.HttpServerSession"; Interlocked.Increment(ref s_nThreads); while (s_nThreads >= (osVersion.is32BitProcess ? 200 : 2000)) 200.ms(); } } finally { server.Stop(); } } static int s_nThreads; TcpClient _client; NetworkStream _ns; HSMessage _message; //current message /// /// Gets the object of this session. /// protected TcpClient Client => _client; /// /// Gets the IP address of the client. /// protected IPAddress ClientIP => ((System.Net.IPEndPoint)_client.Client.RemoteEndPoint).Address; /// /// Print warning when something fails. This is for debug. /// protected bool Verbose { get; set; } /// /// Keep-alive timeout, in milliseconds. Default 10000. /// protected int KeepAliveTimeout { get; set; } = 10_000; /// /// Called when the server receives a HTTP request message. /// /// Contains request info and content. /// Allows to set response info and content. /// /// Not called if failed to read the message. /// /// The server uses try/catch when calling this. Prints unhandled exceptions if true. On unhandled exception sends error 500 (InternalServerError) and closes the connection. /// protected abstract void MessageReceived(HSMessage m, HSResponse r); /// /// Performs basic authentication. If fails (either the client did not use basic authentication or auth returned false), throws exception. The client will receive error 401 (Unauthorized) and can retry. /// /// Callback function. Receives the user name and password. Returns true to continue or false to fail. /// /// After successful authentication does not repeat it again when the client sends more messages in this session. /// /// protected void Auth(Func auth) { if (_auth != true) { _auth = _message.Headers.TryGetValue("Authorization", out var s) && s.Split2_(' ', out var s1, out var s2, 5, 1) && s1.Eqi("Basic") && Convert.FromBase64String(s2).ToStringUTF8().Split2_(':', out s1, out s2, 0, 0) && auth(s1, s2); if (_auth == false) throw new UnauthorizedAccessException(); } } bool? _auth; void _Run(TcpClient client) { _client = client; _client.ReceiveTimeout = 30_000; //tested: throws when the socked receives 0 bytes in this time. It's not the total time of a Read. _client.SendTimeout = 30_000; //it seems this is how long the socket waits until previous chunk is sent. We send in max 64 KB chunks. try { Run(); } catch (Exception e1) { if (Verbose) print.warning(e1, "HttpServerSession: "); } _client.Close(); } /// /// Executes the session: reads requests, calls your , writes responses, implements keep-alive. /// /// /// The server uses try/catch when calling this. Prints unhandled exceptions if true. On unhandled exception closes the connection. /// [SkipLocalsInit] protected virtual void Run() { _ns = _client.GetStream(); g1: HttpStatusCode status; try { status = _ReadRequest(); } catch (HttpReadException_ eh) { status = eh.status; } if (status == HttpReader_.Disconnected) return; HSResponse response = new() { Status = status }; if (status == HttpStatusCode.OK) { try { MessageReceived(_message, response); } catch (UnauthorizedAccessException) when (_auth == false) { _auth = null; response.Status = HttpStatusCode.Unauthorized; response.Headers["WWW-Authenticate"] = "Basic"; } catch (Exception e1) { if (Verbose) print.warning(e1, "HttpServerSession: "); if (response.Status == HttpStatusCode.OK) response.Status = HttpStatusCode.InternalServerError; } } bool keepAlive = KeepAliveTimeout > 0 && status == HttpStatusCode.OK && !(_message.Headers.TryGetValue("Connection", out var v1) && v1.Eqi("close")); if (!keepAlive) response.Headers["Connection"] = "close"; _WriteResponse(response); _message = null; if (keepAlive) { if (_ns.Socket.Poll(Math.Min(KeepAliveTimeout, int.MaxValue / 1000) * 1000, SelectMode.SelectRead) && _ns.DataAvailable) goto g1; //else timeout or closed or error } else if (status != HttpStatusCode.OK) { //Cannot close until the client writes all data and reads the response. Else instead of response it may receive error 'connection reset'. // On error often not all request bytes are read. And the size of this message is unknown. // Read all remaining request bytes until the client closes connection. // Client should close when it reads the response, because: 1. It's an error. 2. We send 'Connection: close'. // tested: _ns.Socket.LingerState does not work. // not tested (because this code works well): _ns.Close(timeout);. _client.ReceiveTimeout = 10_000; //if client never closes, we'll get timeout exception Span b = stackalloc byte[0x4000]; while (_ns.Read(b) != 0) { } } } void _WriteResponse(HSResponse r) { r.Headers.TryAdd("Date", DateTime.UtcNow.ToString("R")); r.Headers.TryAdd("Server", "Au"); byte[] content = r.Content; bool hasContent = !content.NE_(); if (hasContent) { //r.Headers.TryAdd("Content-Type", "Content-Type: text/plain; charset=utf-8"); //no. Server cannot guess. The HTML default is application/octet-stream. //compress if (content.Length > 1000 && _message.Headers.TryGetValue("Accept-Encoding", out var ae) && !r.Headers.ContainsKey("Content-Encoding")) { var h = ae.Lower().Split(new char[] { ',', ';' }, StringSplitOptions.TrimEntries); //use ';' to split items like "br;q=1.0", and ignore q var (content2, s2) = h.Contains("br") ? (Convert2.BrotliCompress(content), "br") : h.Contains("gzip") ? (Convert2.GzipCompress(content), "gzip") : h.Contains("deflate") ? (Convert2.DeflateCompress(content), "deflate") : (content, null); if (content2.Length < content.Length) { content = content2; r.Headers["Content-Encoding"] = s2; } } } r.Headers["Content-Length"] = content.Lenn_().ToS(); //if no content, set 0, else client may wait even if there is empty line int bs = 50 + r.Reason.Lenn() + r.Headers.Sum(o => o.Key.Length + o.Value.Length + 4); using (var w = new StreamWriter(_ns, Encoding.Latin1, bs, leaveOpen: true)) { w.Write($"HTTP/1.1 {(int)r.Status} {r.Status}"); if (!r.Reason.NE()) w.Write(", " + r.Reason); w.WriteLine(); foreach (var (k, v) in r.Headers) w.WriteLine(k + ": " + v); w.WriteLine(); } if (hasContent) { //perf.first(); //_ns.Write(content); //does not wait until client reads all data. Usually returns after ~1 ms regardless of size etc. for (int i = 0; i < content.Length;) { int n = Math.Min(0x10000, content.Length - i); _ns.Write(content, i, n); //waits if previous buffer still not sent i += n; } //perf.nw(); } } [SkipLocalsInit] unsafe HttpStatusCode _ReadRequest() { _message = new(); HttpReader_ reader = new(_ns); //read the request line { string s; do s = reader.ReadLine(); while (s.Length == 0); if (!s.RxMatch(@"^(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH) (.+) HTTP/(\d\.\d)$", out RXMatch m)) return HttpStatusCode.BadRequest; if (m[3].Value is not ("1.1" or "1.0")) return HttpStatusCode.HttpVersionNotSupported; _message.Method = m[1].Value; _message.RawTarget = s = m[2].Value; int i = s.IndexOf('?'); if (i >= 0) { _message.UrlParameters = HSMessage.ParseUrlParameters_(s.AsSpan(i + 1)); s = s[..i]; } _message.TargetPath = WebUtility.UrlDecode(s); } //read headers var headers = _message.Headers; reader.ReadHeaders(headers); //read content _message.Content = reader.ReadContent(headers); //print.it("Target", message.RawTarget, message.TargetPath, message.UrlParameters); //print.it("Headers", message.Headers); //print.it("Content", message.ContentText); return HttpStatusCode.OK; } } /// /// Reads an HTTP request or response message. /// All functions may throw and exceptions of NetworkStream.Read. /// The class was designed to read request, therefore throws exceptions such as BadRequest, but can be used to read response too. /// Uses 16 KB of stack. Consider [SkipLocalsInit]. /// unsafe ref struct HttpReader_ { readonly NetworkStream _ns; Span _b, _line; int _n; //current buffered bytes int _pos; //current position in _b (0.._n) const int c_bsize = 0x4000; //16 K fixed byte __b[c_bsize]; public const HttpStatusCode Disconnected = 0; public HttpReader_(NetworkStream ns) { _ns = ns; fixed (byte* p = __b) _b = new(p, c_bsize); } void _Buffer() { if (_n > 0) { _b = _b.Slice(_n); if (_b.IsEmpty) { if (_line.Length == c_bsize) throw new HttpReadException_(HttpStatusCode.RequestHeaderFieldsTooLarge); fixed (byte* p = __b) _b = new(p, c_bsize); _line.CopyTo(_b); int ll = _line.Length; _line = _b; _b = _b.Slice(ll); } _pos = 0; } _n = _ns.Read(_b); if (_n == 0) throw new HttpReadException_(Disconnected); } //note: would be simpler with _ns.ReadByte(), but it is slow. Therefore we _ns.Read() chunks into __b, and then read bytes from there. int _ReadByte() { if (_pos == _n) _Buffer(); return _b[_pos++]; } public string ReadLine() { _line = _b.Slice(_pos); for (int len = 0; ; len++) { int c = _ReadByte(); if (c is 10 or 13) { _ReadRN(c); return Encoding.Latin1.GetString(_line.Slice(0, len)); } } } void _ReadRN(int c) { if (c == 10) return; if (c == 13 && _ReadByte() == 10) return; throw new HttpReadException_(HttpStatusCode.BadRequest); } public void ReadRN() { _line = _b.Slice(_pos); _ReadRN(_ReadByte()); } public byte[] Read(int length) { var a = GC.AllocateUninitializedArray(length); Span span = a; //at first read from _b int n = _n - _pos; if (n > 0) { n = Math.Min(n, length); _b.Slice(_pos, n).CopyTo(a); _pos += n; span = span.Slice(n); } //then read from _ns, if did not read all from _b while (!span.IsEmpty) { n = _ns.Read(span); if (n == 0) throw new HttpReadException_(Disconnected); span = span.Slice(n); } return a; } /// /// Reads headers until empty line. /// /// The function adds headers here. public void ReadHeaders(Dictionary headers) { string header = null; for (; ; ) { var s = ReadLine(); if (s.Length == 0) break; if (s[0] is ' ' or '\t') { if (header == null) throw new HttpReadException_(HttpStatusCode.BadRequest); header = string.Concat(header, " ", s.AsSpan(1)); } else { _Header(); header = s; } } _Header(); void _Header() { if (header != null) { int i = header.IndexOf(':'); if (i <= 0 || header[i - 1] <= ' ') throw new HttpReadException_(HttpStatusCode.BadRequest); var k = header[..i]; var v = header.AsSpan(i + 1).Trim().ToString(); if (!headers.TryAdd(k, v)) headers[k] += ", " + v; } } } /// /// Reads content and trailing headers. /// /// The function gets content length etc from here. Also reads trailing headers here. /// null if headers don't contain content-length or transfer-encoding. public byte[] ReadContent(Dictionary headers) { byte[] content = null; //get content info etc from headers int contentLength = 0; bool chunked = false, trailer = false; foreach (var (k, v) in headers) { if (k.Eqi("Content-Length")) contentLength = v.ToInt(); else if (k.Eqi("Transfer-Encoding")) { if (v.Eq("chunked")) chunked = true; else throw new HttpReadException_(HttpStatusCode.NotImplemented, k); } else if (k.Eqi("Trailer")) trailer = true; } //read content if (chunked || contentLength > 0) { if (chunked) { List chunks = new(); for (; ; ) { var s = ReadLine(); if (!s.ToInt(out int len, flags: STIFlags.IsHexWithout0x | STIFlags.DontSkipSpaces)) throw new HttpReadException_(HttpStatusCode.BadRequest); if (len == 0) break; chunks.Add(Read(len)); ReadRN(); } if (chunks.Count > 0) { byte[] ac = chunks[0]; if (chunks.Count > 1) { ac = new byte[chunks.Sum(o => o.Length)]; int i = 0; foreach (var a in chunks) { a.CopyTo(ac, i); i += a.Length; } } content = ac; } if (trailer) { //headers after content ReadHeaders(headers); } else { ReadRN(); } } else { content = Read(contentLength); } } return content; } } class HttpReadException_ : Exception { public HttpReadException_(HttpStatusCode status, string message = null) : base(message) { this.status = status; } public readonly HttpStatusCode status; } } namespace Au.Types { /// /// See . /// public class HSMessage { /// /// Method, like "GET" or "POST". /// public string Method { get; internal set; } /// /// Target, like "/file.html" or "/file.html?a=1&b=2" or "/". May be URL-encoded. /// public string RawTarget { get; internal set; } /// /// Target without URL parameters, like "/file.html" or "/". Not URL-encoded. /// public string TargetPath { get; internal set; } /// /// URL parameters (query string). Not URL-encoded. /// /// null if there are no URL parameters. public Dictionary UrlParameters { get; internal set; } /// /// Headers. Case-insensitive. /// public Dictionary Headers { get; } = new(StringComparer.OrdinalIgnoreCase); /// /// Raw content. For example POST data as UTF-8 text or binary. /// /// null if the message is without content. public byte[] Content { get; internal set; } /// /// Content-Type header info. /// /// null if Content-Type header is missing or invalid. public HSContentType ContentType => _contentType ??= HSContentType.Create(Headers); HSContentType _contentType; /// /// converted to text. /// /// null if there is no content. /// If text encoding unspecified, uses UTF-8; if specified invalid, uses ASCII. public string Text => _contentText ??= Content == null ? null : (ContentType?.Encoding ?? Encoding.UTF8).GetString(Content); string _contentText; /// /// JSON-deserializes to object of type T. /// /// default(T) if the request does not have body data. /// Exceptions of . public T Json() => Content == null ? default : JsonSerializer.Deserialize(Content, InternetUtil_.JsonSerializerOptions); /// /// JSON-deserializes to object of specified type. /// /// null if the request does not have body data. /// Exceptions of . public object Json(Type type) => Content == null ? default : JsonSerializer.Deserialize(Content, type, InternetUtil_.JsonSerializerOptions); /// /// Keys/values from POST content with Content-Type: application/x-www-form-urlencoded. /// /// null if the message has no content of this type. public Dictionary Urlencoded { get { if (_contentUrlParameters == null && Content != null && Headers.TryGetValue("Content-Type", out var v) && v.Starts("application/x-www-form-urlencoded", true)) { _contentUrlParameters = ParseUrlParameters_(Encoding.Latin1.GetString(Content)); } return _contentUrlParameters; } } Dictionary _contentUrlParameters; internal static Dictionary ParseUrlParameters_(RStr s) { Dictionary d = null; for (int i = 0, j; i < s.Length; i = j + 1) { int q = -1; for (j = i; j < s.Length && s[j] != '&'; j++) if (s[j] == '=' && q < 0) q = j; if (q > 0) (d ??= new())[WebUtility.UrlDecode(s.Slice(i, q - i).ToString())] = WebUtility.UrlDecode(s.Slice(++q, j - q).ToString()); } return d; } /// /// Parts of multipart content. For example of POST content with Content-Type: multipart/form-data. /// /// null if the message has no multipart content. public Dictionary Multipart { get { if (_contentParts == null && Content != null && Headers.TryGetValue("Content-Type", out var v) && v.Starts("multipart/", true)) { _contentParts = _GetContentMultipart(); } return _contentParts; } } Dictionary _contentParts; Dictionary _GetContentMultipart() { if (Content == null || ContentType?.Boundary is not string sb || Content.Length < sb.Length * 2 + 8) return null; //print.it($"'{Content.ToStringUTF8()}'"); Dictionary a = null; //need to parse bytes, not string, because part bodies can be binary or use various encodings RByte k = Content, b = Encoding.Latin1.GetBytes("--" + sb), b0 = b.Slice(2); if (!_FindBound(k, b, 0, out int startBound, out int endBound, out bool last)) return null; for (int index = 0; !last;) { int startPart = endBound; if (!_FindBound(k, b, startPart, out startBound, out endBound, out last)) return null; var part = k.Slice(startPart, startBound - startPart); //print.it($"<<{part.ToStringUTF8()}>>"); int i = 0; if (!part.StartsWith("\r\n"u8)) { i = part.IndexOf("\r\n\r\n"u8) + 2; if (i < 2) continue; } var dh = new Dictionary(StringComparer.OrdinalIgnoreCase); if (i != 0) { var sh = Encoding.Latin1.GetString(part.Slice(0, i - 2)).RxReplace(@"\n\h", " "); foreach (var v in sh.Lines(true)) { if (v.Split2_(':', out var s1, out var s2, 1, 0)) dh[s1] = s2; } } HSContentPart p = new(index++, dh, part.Slice(i + 2).ToArray()); (a ??= new())[p.Name] = p; } return a; static bool _FindBound(RByte k, RByte b, int i, out int start, out int end, out bool last) { start = end = 0; last = false; for (; i < k.Length; i = end) { int j = k.Slice(i).IndexOf(b); if (j < 0) break; start = i + j; end = start + b.Length + 2; if (end > k.Length) break; if (start == 0 || (start >= 2 && k[start - 1] == '\n' && k[start - 2] == '\r')) { if (start >= 2) start -= 2; if (k[end - 1] == '\n' && k[end - 2] == '\r') return true; if (k[end - 1] == '-' && k[end - 2] == '-') { last = true; return true; } } } return false; } } } /// /// Contains a single part of a multipart POST data. /// /// 0-based index of this part in the list of parts. /// Headers of this part. /// Raw content of this part. For example UTF-8 text. public record class HSContentPart(int Index, Dictionary Headers, byte[] Content) { /// public HSContentType ContentType => _contentType ??= HSContentType.Create(Headers); HSContentType _contentType; /// /// converted to text. /// /// null if there is no content. /// If text encoding unspecified, uses UTF-8; if specified invalid, uses ASCII. public string Text => _contentText ??= Content == null ? null : (ContentType?.Encoding ?? Encoding.UTF8).GetString(Content); string _contentText; System.Net.Mime.ContentDisposition _ContentDisposition() { if (_contentDisposition == null && Headers.TryGetValue("Content-Disposition", out var s)) try { _contentDisposition = new(s); } catch { } return _contentDisposition; } System.Net.Mime.ContentDisposition _contentDisposition; /// /// Gets name from Content-Disposition header. /// /// If Content-Disposition header or name is missing, returns Index.ToS(). /// /// Decodes "=?utf-8?B?base64?=". /// public string Name => _name ??= _DecodeMime(_ContentDisposition()?.Parameters["name"] ?? Index.ToS()); string _name; /// /// Gets filename from Content-Disposition header. /// /// null if Content-Disposition header or filename is missing. /// /// Decodes "utf-8''urlencoded" or "=?utf-8?B?base64?=". /// public string FileName { get { if (_fileName == null && _ContentDisposition() is { } cd) { if (cd.Parameters["filename*"] is string s && s.Starts("utf-8''")) { //never mind: can be "any-charset'language'" try { _fileName = WebUtility.UrlDecode(s[7..]); } catch { } } _fileName ??= _DecodeMime(cd.Parameters["filename"]); } return _fileName; } } string _fileName; static string _DecodeMime(string s) { if (s != null && s.Starts("=?utf-8?B?", true) && s.Ends("?=")) { try { return Convert.FromBase64String(s[10..^2]).ToStringUTF8(); } catch { } } return s; } } /// /// Contains properties of HTTP Content-Type header. /// See . /// public class HSContentType { /// /// Creates from Content-Type header. /// /// null if Content-Type header is missing or invalid. public static HSContentType Create(Dictionary headers) { if (headers.TryGetValue("Content-Type", out var s)) { try { return new(new(s)); } catch { } } return null; } HSContentType(System.Net.Mime.ContentType t) { MediaType = t.MediaType; Boundary = t.Boundary; Charset = t.CharSet; Encoding = _GetEncoding(t); } /// public string MediaType { get; } /// /// Returns the boundary parameter without double quotes, or null if not specified. /// public string Boundary { get; } /// /// Returns the charset parameter, or null if not specified. /// public string Charset { get; } /// /// Gets text encoding. /// /// Returns: ///
null if multipart content ( not null). ///
• UTF-8 if charset is utf-8 or not specified. ///
Encoding that matches charset. ///
• ASCII if charset is invalid. ///
public Encoding Encoding { get; } Encoding _GetEncoding(System.Net.Mime.ContentType t) { var s = Charset; if (s == null) return t.Boundary != null ? null : Encoding.UTF8; if (s.Eqi("utf-8")) return Encoding.UTF8; if (s.Eqi("us-ascii")) return Encoding.ASCII; if (s.Eqi("iso-8859-1")) return Encoding.Latin1; return StringUtil.GetEncoding(s) ?? Encoding.ASCII; } } /// /// See . /// public class HSResponse { /// /// Response status code. Initially OK. /// public HttpStatusCode Status { get; set; } /// /// Response reason phrase. Initially null. /// public string Reason { get; set; } /// /// Response headers. Initially empty. /// The server later may add Date, Server, Content-Encoding, Content-Length. /// public Dictionary Headers { get; } = new(StringComparer.OrdinalIgnoreCase); /// /// Raw response content. /// /// /// /// /// /// The server may send this data compressed (it depends on headers etc). /// public byte[] Content { get; set; } /// /// Sets response content text. /// /// Sets : Content = content.ToUTF8();. /// If not null, sets Content-Type header. public void SetContentText(string content, string contentType = "text/plain; charset=utf-8") { Content = content?.ToUTF8(); if (contentType != null) Headers["Content-Type"] = contentType; } /// /// JSON-serializes object of type T, and sets . Also sets Content-Type header. /// /// Object of type T. /// If not null, sets Content-Type header. /// Exceptions of . public void SetContentJson(T obj, string contentType = "application/json; charset=utf-8") { Content = JsonSerializer.SerializeToUtf8Bytes(obj, InternetUtil_.JsonSerializerOptions); if (contentType != null) Headers["Content-Type"] = contentType; } /// /// JSON-serializes object of specified type, and sets . Also sets Content-Type header. /// /// Object. /// obj type. /// If not null, sets Content-Type header. /// Exceptions of . public void SetContentJson(object obj, Type type, string contentType = "application/json; charset=utf-8") { Content = JsonSerializer.SerializeToUtf8Bytes(obj, type, InternetUtil_.JsonSerializerOptions); if (contentType != null) Headers["Content-Type"] = contentType; } } static class InternetUtil_ { static readonly Lazy s_defaultSerializerOptions = new(() => new(JsonSerializerDefaults.Web)); public static JsonSerializerOptions JsonSerializerOptions => s_defaultSerializerOptions.Value; } } ================================================ FILE: Au/Au.More/IconImageCache.cs ================================================ //TODO2: 2025-11: often the main floating toolbar expands slowly. 2025-12: fast again. using System.Drawing; using Microsoft.Win32; namespace Au.More; /// /// Gets images as of same logical size to be displayed as icons. Can get file icons or load images from files etc. /// /// /// Uses memory cache and optionally file cache to avoid loading same image multiple times. Getting images from cache is much faster. /// /// Bitmap objects retrieved by this class are stored in memory cache. Don't dispose them before disposing the IconImageCache object. Usually don't need to dispose these Bitmap objects explicitly (GC will do it). /// /// Thread-safe. /// public sealed class IconImageCache : IDisposable { record class _DpiImages { readonly IconImageCache _cache; public readonly int dpi; public readonly string indexFile; public Dictionary dNameHash; //index file data public readonly Dictionary dNameBitmap = new(); //memory cache public readonly Dictionary dHashBitmap = new(); //let all identical images share single Bitmap object //info: dictionary string keys are case-sensitive. We have not only paths but also base64 MD5. For paths we call Lower. public _DpiImages(IconImageCache cache, int dpi) { _cache = cache; this.dpi = dpi; if (_cache._dir is string s) indexFile = s + "\\" + dpi.ToS() + ".dpi"; } /// /// If the index file for this DPI still not loaded, loads it into dNameHash. /// public void LoadIndexFile() { if (dNameHash == null && filesystem.exists(indexFile, useRawPath: true)) { try { bool save = false; Dictionary d = new(StringComparer.OrdinalIgnoreCase); filesystem.waitIfLocked(() => { var fs = File.OpenRead(indexFile); using var br = new BinaryReader(fs); for (var len = fs.Length; fs.Position < len;) { var imageKey = br.ReadString(); Hash.MD5Result hash = new(br.ReadInt64(), br.ReadInt64()); ref var r = ref d.GetValueRefOrAddDefault_(imageKey, out bool exists); r = hash; if (exists) save = true; //save without duplicates. Keep the newest hash. } }); dNameHash = d; if (save) _SaveIndex(); } catch (Exception e1) { Debug_.Print(e1); } } dNameHash ??= new(); } void _SaveIndex() { //print.it("<>SaveIndex<>"); filesystem.waitIfLocked(() => { var fs = File.Create(indexFile); using var bw = new BinaryWriter(fs); foreach (var v in dNameHash) { bw.Write(v.Key); bw.Write(v.Value.r1); bw.Write(v.Value.r2); } }); } public void AppendToIndexFile(string imageKey, Hash.MD5Result hash) { filesystem.waitIfLocked(() => { var fs = File.Open(indexFile, FileMode.Append); using var bw = new BinaryWriter(fs); bw.Write(imageKey); bw.Write(hash.r1); bw.Write(hash.r2); }); } } readonly string _dir; readonly List<_DpiImages> _aDpi = new(); //for each used DPI readonly int _imageSize; bool _disposed, _onceUsedFiles; readonly HashSet _extDynamicIcon = new(); static List> s_caches = new(); /// Width and height of images. Min 16, max 256. /// Path of cache directory. If null, will be used only memory cache. /// Not full path. public IconImageCache(int imageSize = 16, string directory = null) { if (imageSize < 16 || imageSize > 256) throw new ArgumentOutOfRangeException(nameof(imageSize)); _imageSize = imageSize; //directory = null; //test memory-only cache if (directory != null) _dir = pathname.normalize(directory); lock (s_caches) s_caches.Add(new(this)); //_InitNotifyWindow(); script.GetAuxThread_(); //ensure the aux thread is running. It calls ClearAll_ when receives message "clear image caches". } /// /// Width and height of images. /// public int ImageSize => _imageSize; //public string CacheDirectory => _dir; /// /// Common cache for icons of size 16. Used by menus, toolbars and editor. /// /// /// Uses cache directory folders.ThisAppDataLocal + "iconCache". /// public static IconImageCache Common => CommonOfSize(16); /// /// Common cache for icons of given size. Used by menus and toolbars if custom . /// /// Width and height of images. Min 16, max 256. public static IconImageCache CommonOfSize(int imageSize) { if (imageSize < 16 || imageSize > 256) throw new ArgumentOutOfRangeException(nameof(imageSize)); return s_commonDict.GetOrAdd(imageSize, o => new(o, folders.ThisAppDataLocal + "iconCache" + (o == 16 ? "" : o.ToS()))); } static ConcurrentDictionary s_commonDict = new(); //rejected: if this is a portable app in a removable drive, use only memory cache (dir=null). Maybe only if folders.ThisAppDataLocal is in a fixed drive. // Cannot reliably detect whether this app is portable. And whether the app (or user) wants to use the file cache. // Eg this app may be in an external SSD drive, but external SSD drives are detected as fixed, and impossible to know whether it is used as a portable app. // Instead, if this is a portable app and does not want to write in other drives, let it set folders.ThisAppDataLocal. And portable LA does it (as well as script processes started from it). /// /// Gets image from cache or file etc. /// /// File path, or resource path that starts with "resources/" or has prefix "resource:", or icon name like "*Pack.Icon color", etc. See isImage parameter. /// DPI of window that will display the image. See . /// /// false - get file/folder/filetype/url/etc icon with . If imageSource is relative path of a .cs file, gets its custom icon as image; returns null if no custom icon or if editor isn't running. /// true - load image from xaml/png/etc file, database, resource or string with or . Can be icon name like "*Pack.Icon color" (see menu Tools > Icons). /// /// To detect whether a string is an image, call ; if it returns true, it is image. /// /// Action to call when fails to load image. If null, then silently returns null. Parameters are image source string and exception. public unsafe Bitmap Get(string imageSource, int dpi, bool isImage, Action onException = null) { //print.it(imageSource, isImage); if (_disposed) throw new ObjectDisposedException(nameof(IconImageCache)); bool isXaml = isImage && (imageSource.Starts('<') || imageSource.Ends(".xaml", true)); bool isStore = !isImage && imageSource.Starts(@"shell:AppsFolder\"); //compare case-sensitive. Then users can pass eg "shell:appsFolder..." to display white icons in blue background. bool ofWorkspaceFile = false; if (!isImage && !isStore && imageSource.Ends(".cs", true) && !pathname.isFullPath(imageSource, orEnvVar: true)) { //eg `script.run(@"x.cs");` imageSource = ScriptEditor.GetIcon(imageSource, EGetIcon.PathToIconName); if (imageSource == null) return null; isImage = ofWorkspaceFile = true; //rejected: use Dictionary to avoid frequent GetIcon for same imageSource. In LA process fast, elsewhere not too slow. //rejected: Move this code to the caller that needs it (MTBase). } bool isIconName = isImage && !isXaml && imageSource.Starts('*'); if (isIconName) { isXaml = true; imageSource = IconString_.NormalizeColor(imageSource); //color can be "normal|highContrast" } if (!isXaml && !isStore) dpi = 96; //will scale when drawing, it's fast and not so bad. Tested scaling with Lanczos3 etc filters, but the result for icons isn't better. string imageKey = imageSource; if (!isIconName) { if ((isXaml && imageKey.Starts('<')) || (isImage && ImageUtil.HasImageStringPrefix(imageKey))) imageKey = Hash.MD5(imageSource, base64: true); else imageKey = imageKey.Lower(); } bool isIco = !isImage && !isStore && imageKey.Ends(".ico"); lock (this) { //get _DpiImages for dpi _DpiImages dd; foreach (var v in _aDpi) if (v.dpi == dpi) { dd = v; goto g1; } _aDpi.Add(dd = new _DpiImages(this, dpi)); g1: if (dd.dNameBitmap.TryGetValue(imageKey, out var b)) return b; //print.it(imageSource, isImage); //using var p1 = perf.local(); //bool useHash = !isImage && !isIco; bool useHash = isImage ? isIconName : !isIco; //use file cache for *icon too //use file cache for *icon too. // Because: // Loads XAML icon slowly first time (~100 ms; not measured after reboot), even in LA, and even later loads slower than from the cache file. // Non-WPF process uses much more memory (because loads WPF), eg 14 -> 28 MB. // Bad: when trying to find icons, users try many icons, colors, sizes. Then the cache is full of garbage. // TODO3: remove from cache if not using anymore. Or add only if frequently using. //FUTURE: to make loading XAML icons faster etc, try Windows.UI.Xaml.Markup.XamlReader.Load. Use Microsoft.Windows.SDK.NET.dll, or directly COM if possible. When the library will not support Win7/8. bool useFile = _dir != null && useHash; if (useFile) { try { if (!_onceUsedFiles) { _onceUsedFiles = true; var fe = filesystem.exists(_dir); if (fe.File) filesystem.delete(_dir); //fbc (was .db file) if (!fe.Directory) filesystem.createDirectory(_dir); _InitFiles(); } dd.LoadIndexFile(); bool useExt = false; g2: if (dd.dNameHash.TryGetValue(imageKey, out var hash)) { if (dd.dHashBitmap.TryGetValue(hash, out var bb)) return bb; var path = _dir + "\\" + hash.ToString() + ".png"; try { //b = (Bitmap)Image.FromFile(path); //no, locks file using (var stream = File.OpenRead(path)) b = (Bitmap)Image.FromStream(stream); dd.dNameBitmap[imageKey] = b; dd.dHashBitmap[hash] = b; return b; } catch (Exception e1) { Debug_.PrintIf(filesystem.exists(path), e1); } } else if (!useExt && !isImage && !isStore && !imageKey.Ends(".exe") && !imageKey.Ends(".lnk") && pathname.isFullPath(imageSource) && filesystem.exists(imageSource, useRawPath: true).File) { var ext = pathname.getExtension(imageKey); bool noExt = ext.Length == 0; if (noExt) ext = ".#"; //will get the icon of unknown file types if (dd.dNameBitmap.TryGetValue(ext, out var b1)) return b1; if (!_extDynamicIcon.Contains(ext)) { if (!noExt && _DynamicIcon(ext)) _extDynamicIcon.Add(ext); else { imageKey = imageSource = ext; useExt = true; goto g2; } } } } catch (Exception e1) { print.warning(e1); useFile = false; } //failed to create _dir } //p1.Next('C'); try { //long t1 = perf.mcs; if (!isImage) { b = isStore ? icon.winStoreAppImage(imageSource, Dpi.Scale(_imageSize, dpi)) : null; b ??= icon.of(imageSource, _imageSize)?.ToGdipBitmap(); } else { if (isIconName) { imageSource = ScriptEditor.GetIcon_(imageSource, EGetIcon.IconNameToXaml, skipResources: ofWorkspaceFile); //p1.Next('X'); if (imageSource == null) return null; } if (isXaml) b = ImageUtil.LoadGdipBitmapFromXaml(imageSource, dpi, (_imageSize, _imageSize)); else b = ImageUtil.LoadGdipBitmap(imageSource); } //if (useFile) useFile = perf.mcs - t1 > 1000; //reduces the index file size in worst cases, but makes significantly slower later } catch (Exception ex) { if (onException != null) onException(imageSource, ex); //else print.warning("IconImageCache.Get() failed. " + ex.ToStringWithoutStack()); //no. Often prints while editing text if editor shows images in text. } //p1.Next('L'); if (b != null && (isImage ? useFile : !isIco)) { try { var ms = new MemoryStream(); b.Save(ms, System.Drawing.Imaging.ImageFormat.Png); //~200 mcs. It's fast if compared with icon.of etc and saving. var hash = Hash.MD5(ms.GetBuffer().AsSpan(0, (int)ms.Position)); ref var br = ref dd.dHashBitmap.GetValueRefOrAddDefault_(hash, out bool exists); if (!exists) br = b; else { b.Dispose(); b = br; } if (useFile) { //p1.Next(); if (!exists) { //print.it("<>save<>"); filesystem.waitIfLocked(() => { using var fs = File.Create($@"{_dir}\{hash.ToString()}.png"); fs.Write(ms.GetBuffer().AsSpan(0, (int)ms.Position)); }); } dd.dNameHash[imageKey] = hash; //p1.Next(); dd.AppendToIndexFile(imageKey, hash); } } catch (Exception e1) { Debug_.Print(e1); } } dd.dNameBitmap[imageKey] = b; return b; } //rejected: Don't cache if non-literal (non-interned) string. Caller may generate many random strings, eg icon colors. Impossible to detect reliably. static bool _DynamicIcon(string ext) { if (Registry.GetValue(@"HKEY_CLASSES_ROOT\" + ext, "", null) is string s1) { //if (Registry.GetValue(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\" + ext + @"\UserChoice", "ProgId", null) is string s2) return _DI(s2); //it seems Windows ignores this when looking for icon handler or %1 if (_DI(s1)) return true; } return false; static bool _DI(string progid) => Registry.GetValue(@"HKEY_CLASSES_ROOT\" + progid + @"\ShellEx\IconHandler", "", null) is string || Registry.GetValue(@"HKEY_CLASSES_ROOT\" + progid + @"\DefaultIcon", "", null) is "\"%1\"" or "%1"; //exe, ico, cur, ani } } void _InitFiles() { if (_dir == null) return; //delete cache files if changed OS/.NET/Au version var verPath = _dir + @"\version.txt"; try { var sNew = osVersion.onaString; if (filesystem.exists(verPath, useRawPath: true)) { var sOld = filesystem.loadText(verPath); if (sNew == sOld) return; _ClearFiles(); } filesystem.saveText(verPath, sNew); } catch (Exception e1) { Debug_.Print(e1); } } /// /// Removes images from memory cache (but does not dispose) and makes this object unusable. /// Optional. /// public void Dispose() { _disposed = true; _aDpi.Clear(); _Dispose(); GC.SuppressFinalize(this); } /// ~IconImageCache() { /*print.it("~IconImageCache");*/ _Dispose(); } void _Dispose() { lock (s_caches) { for (int i = s_caches.Count; --i >= 0;) { if (!s_caches[i].TryGetTarget(out var v) || v == this) s_caches.RemoveAt(i); } } } /// /// Clears the cache (removes images from memory cache and file cache). /// /// Redraw (asynchronously) all visible windows of this process. public void Clear(bool redrawWindows = false) { if (_disposed) throw new ObjectDisposedException(nameof(IconImageCache)); if (_Clear() && redrawWindows) _RedrawWindowsOfThisProcess(); } bool _Clear() { lock (this) { if (_disposed) return false; _aDpi.Clear(); _extDynamicIcon.Clear(); } _ClearFiles(); Cleared?.Invoke(); return true; } /// /// When the cache cleared. /// public event Action Cleared; void _ClearFiles() => _ClearFiles(_dir); static void _ClearFiles(string dir) { if (dir == null || !filesystem.exists(dir, true).Directory) return; try { bool deletedAll = true; foreach (var v in Directory.GetFiles(dir, "*.dpi")) deletedAll &= Api.DeleteFile(v); if (deletedAll) { foreach (var v in Directory.GetFiles(dir, "*.png")) deletedAll &= Api.DeleteFile(v); if (deletedAll && !dir.Ends(@"\iconCache", true) && Api.DeleteFile(dir + @"\version.txt")) Api.RemoveDirectory(dir); } } catch (Exception e1) { Debug_.Print(e1); } } static unsafe void _RedrawWindowsOfThisProcess() { foreach (var w in wnd.findAll(of: WOwner.Process(process.thisProcessId), flags: WFlags.CloakedToo)) Api.RedrawWindow(w, flags: Api.RDW_INVALIDATE | Api.RDW_ALLCHILDREN); } /// /// Clears caches of all instances of this or all processes. Redraws (asynchronously) all visible windows of these processes. /// /// Clear in all processes of this user session. public static void ClearAll(bool allProcesses = true) { ClearAll_(); if (allProcesses) { //if called in LA process (eg menu Tools > Update icons), clear the cache dir of scripts. // Without it would clear only if some script processes already used the cache. if (script.role == SRole.EditorExtension && Common._dir is string s1) { Debug.Assert(s1.Ends(@"\iconCache")); s1 = s1.ReplaceAt(^9.., "_script"); //clear caches of all image sizes var a = filesystem.enumDirectories(s1, "iconCache*").ToArray(); foreach (var f in a) { _ClearFiles(f.FullPath); } } for (var w = wnd.findFast(null, script.c_auxWndClassName, true); !w.Is0; w = wnd.findFast(null, script.c_auxWndClassName, true, w)) if (!w.IsOfThisProcess) w.SendNotify(script.c_msg_IconImageCache_ClearAll); } } internal static void ClearAll_() { List a = new(); lock (s_caches) foreach (var c in s_caches) if (c.TryGetTarget(out var v)) a.Add(v); bool redrawWindows = false; foreach (var v in a) redrawWindows |= v._Clear(); if (redrawWindows) _RedrawWindowsOfThisProcess(); } //static int s_auxInited; //static void _InitNotifyWindow() { // //if (0 == Interlocked.Exchange(ref s_auxInited, 1)) { // // var t = script.GetAuxThread_(); // // t.QueueAPC(_InitAuxThread); // //} // //static void _InitAuxThread() { // // //rejected. Assoc may be changed frequently. The cache probably even does not contain icons of those file types. // // // Anyway cannot auto-clear if changed while cache not running. // // // Also cannot auto-clear when icons changed not because of a changed assoc. Eg changed .exe icon, edited .ico, changed .lnk icon. // // //using var pidl = Pidl.FromString(":: "); // // //var e = new api.SHChangeNotifyEntry { pidl = pidl.UnsafePtr, fRecursive = true }; // // //Api.SHChangeNotifyRegister(script.AuxWnd_, api.SHCNRF_ShellLevel, api.SHCNE_ASSOCCHANGED, script.c_msg_IconImageCache_ClearAll, 1, e); // // ////undocumented: is it important to call SHChangeNotifyDeregister when this process ends? // //} //} } ================================================ FILE: Au/Au.More/ImageUtil.cs ================================================ using System.Windows; using System.Windows.Markup; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Imaging; namespace Au.More; /// /// Loads WPF and GDI+ images from file, resource or string. /// /// public static partial class ImageUtil { /// /// Returns true if string starts with "image:". /// public static bool HasImageStringPrefix(string s) => s.Starts("image:"); /// /// Returns true if string starts with "resource:", "resources/", "image:" (Base64 encoded image), "imagefile:" (file path), "*" (XAML icon name) or "<" (possibly XAML image). /// public static bool HasImageOrResourcePrefix(string s) => s.Starts('*') || s.Starts('<') || s.Starts("image:") || s.Starts("imagefile:") || ResourceUtil.HasResourcePrefix(s); /// /// Loads image file data as stream from Base64 string. /// /// Base64 encoded image string with prefix "image:". /// String does not start with "image:" or is invalid Base64. /// exceptions (when compressed .bmp). public static MemoryStream LoadImageStreamFromString(string s) { if (!HasImageStringPrefix(s)) throw new ArgumentException("String must start with \"image:\"."); int start = 6; while (start < s.Length && s[start] <= ' ') start++; //can be eg "image:\r\n..." bool compressedBmp = s.Eq(start, "WkJN"); if (compressedBmp) start += 4; int n = (int)((s.Length - start) * 3L / 4); var b = new byte[n]; if (!Convert.TryFromBase64Chars(s.AsSpan(start), b, out n)) throw new ArgumentException("Invalid Base64 string"); if (!compressedBmp) return new MemoryStream(b, 0, n, false); return new MemoryStream(Convert2.BrotliDecompress(b.AsSpan(0, n)), false); } /// /// Loads GDI+ image from Base64 string. /// /// Base64 encoded image string with prefix "image:". /// Exceptions of and . static System.Drawing.Bitmap _LoadGdipBitmapFromString(string s) => new(LoadImageStreamFromString(s)); /// /// Loads WPF image from Base64 string. /// /// Base64 encoded image string with prefix "image:". /// Exceptions of and . static BitmapFrame _LoadWpfImageFromString(string s) => BitmapFrame.Create(LoadImageStreamFromString(s)); //not used in library ///// ///// Calls and handles exceptions. On exception returns null and optionally prints a warning. ///// //public static System.Drawing.Bitmap TryLoadGdipBitmapFromString(string s, bool warning) { // try { return LoadGdipBitmapFromString(s); } // catch (Exception ex) { if (warning) print.warning(ex.ToStringWithoutStack()); } // return null; //} ///// ///// Calls and handles exceptions. On exception returns null and optionally prints warning. ///// //public static BitmapFrame TryLoadWpfImageFromString(string s, bool warning) { // try { return LoadWpfImageFromString(s); } // catch (Exception ex) { if (warning) print.warning(ex.ToStringWithoutStack()); } // return null; //} /// /// Loads GDI+ image from file, resource or string. /// /// /// Can be: ///
• file path. Can have prefix "imagefile:". ///
• resource path that starts with "resources/" or has prefix "resource:" () ///
• Base64 encoded image string with prefix "image:". /// /// If not null, supports XAML images. See . /// Depending on image string format, exceptions of , , etc. public static System.Drawing.Bitmap LoadGdipBitmap(string image, (int dpi, SIZE? size)? xaml = null) { if (HasImageStringPrefix(image)) return _LoadGdipBitmapFromString(image); if (xaml != null && (image.Starts('<') || image.Ends(".xaml", true))) return LoadGdipBitmapFromXaml(image, xaml.Value.dpi, xaml.Value.size); if (ResourceUtil.HasResourcePrefix(image)) return ResourceUtil.GetGdipBitmap(image); if (image.Starts("imagefile:")) image = image[10..]; image = pathname.normalize(image, folders.ThisAppImages); //return new(image); //no, the file remains locked until the Bitmap is disposed (documented, tested) using var fs = File.OpenRead(image); return new(fs); } /// /// Loads WPF image or icon from file, resource or string. /// /// /// Can be: ///
• file path. Can have prefix "imagefile:". ///
• resource path that starts with "resources/" or has prefix "resource:" () ///
• Base64 encoded image string with prefix "image:". /// /// public static BitmapFrame LoadWpfImage(string image) { if (HasImageStringPrefix(image)) return _LoadWpfImageFromString(image); if (ResourceUtil.HasResourcePrefix(image)) return ResourceUtil.GetWpfImage(image); if (image.Starts("imagefile:")) image = image[10..]; image = pathname.normalize(image, folders.ThisAppImages, flags: PNFlags.CanBeUrlOrShell); //CanBeUrlOrShell: support "pack:" return BitmapFrame.Create(new Uri(image)); //rejected: support XAML and "*iconName". Possible but not easy. Probably would be blurred when autoscaled. } /// /// Loads WPF image element from file, resource or string. Supports xaml, png and other image formats supported by WPF. /// /// /// Can be: ///
• file path. Can be .xaml, .png etc. Supports environment variables etc, see . Can have prefix "imagefile:". ///
• resource path that starts with "resources/" or has prefix "resource:". This function calls if ends with ".xaml", else . ///
• Base64 encoded image with prefix "image:". See . ///
• XAML string that starts with "<". For example from the Icons tool of LibreAutomate. ///
• XAML icon name like "*Pack.Icon color" or "*Pack.Icon color @size" or "*Pack1.Icon1 color1; *Pack2.Icon2 color2 %8,8,,". More info in Remarks. /// /// /// If image is XAML icon name or starts with "<" or ends with ".xaml" (case-insensitive), returns new WPF element of type specified by the XAML root element (uses ). Else returns with Source = (uses ). /// /// /// image can be an XAML icon name from the Icons tool of LibreAutomate (LA), like "*Pack.Icon color". Full format: "[*<library>]*pack.name[ color][ @size][ %margin][;more icons]". Here parts enclosed in [] are optional. The color, size and margin parts can be in any order. ///
• color - #RRGGBB or color name (WPF). If 2 colors like "#008000|#00FF00", the second color is for high contrast dark theme. If omitted, will use the system color of control text. Also can be like "#008000|" to use control text only for dark contrast theme, or "|#00FF00" for vice versa. ///
• size - icon size 1 to 16, like "*Pack.Icon blue @12". Can be used to make the displayed icon smaller or in some cases less blurry. It is the logical width and height of the icon rendered at the center of a box of logical size 16x16. To make icon bigger, instead set properties Width and Height of the returned element; or for a toolbar or menu. ///
• margin - icon margins inside a box of logical size 16x16. Format: %left,top,right,bottom,stretch,snap. All parts are optional. Examples: "*Pack.Icon blue %,,8,8", "*Pack.Icon blue %8,8", "*Pack.Icon blue %4,,4,,f". The stretch part can be f (fill) or m (move); default is uniform. The snap part can be p (sets SnapsToDevicePixels=True). Can be used either margin or size, not both. ///
• more icons - can be specified multiple icons separated by semicolon, like "*Pack1.Icon1 color1; *Pack2.Icon2 color2". It allows to create multi-color icons (for example a "filled" icon of one color + an "outline" icon of another color) or to add a small overlay icon (eg to indicate disabled state) at a corner (use margin). ///
• library - name of assembly containing the resource. If omitted, uses . ///
The LA compiler finds icon strings anywhere in code, gets their XAML from the database, and adds the XAML to the assembly as a string resource (see Properties > Resource > Options). This function gets the XAML from resources (). If fails, then tries to get XAML from database, and fails if LA isn't running. Uses . ///
/// public static FrameworkElement LoadWpfImageElement(string image) { if (image.Starts('*')) { image = ScriptEditor.GetIcon(image, EGetIcon.IconNameToXaml) ?? throw new AuException("*get icon " + image); } if (image.Starts('<')) return (FrameworkElement)XamlReader.Parse(image); if (image.Ends(".xaml", true)) { if (ResourceUtil.HasResourcePrefix(image)) return (FrameworkElement)ResourceUtil.GetXamlObject(image); if (image.Starts("imagefile:")) image = image[10..]; using var stream = File.OpenRead(image); return (FrameworkElement)XamlReader.Load(stream); } else { var bf = LoadWpfImage(image); return new Image { Source = bf }; } //Could set UseLayoutRounding=true as a workaround for blurry images, but often it does not work and have to be set on parent element. // Then does not work even if wrapped eg in a Border with UseLayoutRounding. } /// /// Loads GDI+ image from WPF XAML file or string. /// /// XAML file, resource or string. See . /// DPI of window that will display the image. /// Final image size in logical pixels (not DPI-scaled). If null, uses element's DesiredSize property, max 1024x1024. /// New Bitmap. Note: its pixel format is Format32bppPArgb (premultiplied ARGB). /// /// /// Calls and . /// Don't use the Tag property of the bitmap. It keeps bitmap data. /// [MethodImpl(MethodImplOptions.NoInlining)] public static System.Drawing.Bitmap LoadGdipBitmapFromXaml(string image, int dpi, SIZE? size = null) { var e = LoadWpfImageElement(image); //s_cwt.Add(e, new()); return ConvertWpfImageElementToGdipBitmap(e, dpi, size); } /// /// Converts WPF image element to GDI+ image. /// /// For example . /// DPI of window that will display the image. /// /// Final image size in logical pixels (not DPI-scaled). /// If null, uses element's DesiredSize property, max 1024x1024. /// If not null, sets element's Width and Height; the element should not be used in UI. /// /// New Bitmap. Note: its pixel format is Format32bppPArgb (premultiplied ARGB). public static unsafe System.Drawing.Bitmap ConvertWpfImageElementToGdipBitmap(FrameworkElement e, int dpi, SIZE? size = null) { bool measured = e.IsMeasureValid; if (size != null) { measured = false; e.Width = size.Value.width; e.Height = size.Value.height; } if (!measured) e.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); bool arranged = measured && e.IsArrangeValid; if (!arranged) e.Arrange(new Rect(e.DesiredSize)); if (!arranged) e.UpdateLayout(); //prevent memory leak if (size == null) { var z = e.DesiredSize; //if using RenderSize or ActualX, if element height!=width, draws in wrong place, clipped size = new(Math.Min(1024d, z.Width).ToInt(), Math.Min(1024d, z.Height).ToInt()); } var (wid, hei) = Dpi.Scale(size.Value, dpi); var rtb = new RenderTargetBitmap(wid, hei, dpi, dpi, PixelFormats.Pbgra32); //var rtb = t_rtb ??= new RenderTargetBitmap(wid, hei, dpi, dpi, PixelFormats.Pbgra32); rtb.Clear(); //not better //note: if Bgra32, throws exception "'Bgra32' PixelFormat is not supported for this operation". rtb.Render(e); int stride = wid * 4, msize = hei * stride; var b = new System.Drawing.Bitmap(wid, hei, System.Drawing.Imaging.PixelFormat.Format32bppPArgb); using var d = b.Data(System.Drawing.Imaging.ImageLockMode.ReadWrite); rtb.CopyPixels(new(0, 0, wid, hei), d.Scan0, msize, stride); b.SetResolution(dpi, dpi); return b; //tested: GC OK. Don't need GC_.AddObjectMemoryPressure. WPF makes enough garbage to trigger GC when need. } /// /// Converts WPF image element to native icon file data. /// /// Stream to write icon file data. Writes from start. /// Image element. See . /// Sizes of icon images to add. For example 16, 24, 32, 48, 64. Sizes can be 1 to 256 inclusive. /// An invalid size. /// internal static unsafe void ConvertWpfImageElementToIcon_(Stream stream, FrameworkElement e, int[] sizes) { stream.Position = Math2.AlignUp(sizeof(Api.NEWHEADER) + sizeof(Api.ICONDIRENTRY) * sizes.Length, 4); var a = stackalloc Api.ICONDIRENTRY[sizes.Length]; for (int i = 0; i < sizes.Length; i++) { int size = sizes[i]; if (size < 1 || size > 256) throw new ArgumentOutOfRangeException(); using var b = ImageUtil.ConvertWpfImageElementToGdipBitmap(e, 96, (size, size)); int pos = (int)stream.Position; b.Save(stream, System.Drawing.Imaging.ImageFormat.Png); byte bsize = (byte)(size == 256 ? 0 : checked((byte)size)); a[i] = new Api.ICONDIRENTRY { bWidth = bsize, bHeight = bsize, wBitCount = 32, dwBytesInRes = (int)stream.Position - pos, dwImageOffset = pos }; } var posEnd = stream.Position; stream.Position = 0; var h = new Api.NEWHEADER { wResType = 1, wResCount = (ushort)sizes.Length }; stream.Write(new(&h, sizeof(Api.NEWHEADER))); stream.Write(new(a, sizeof(Api.ICONDIRENTRY) * sizes.Length)); stream.Position = posEnd; } /// /// Converts WPF image element to native icon file. /// /// internal static void ConvertWpfImageElementToIcon_(string icoFile, FrameworkElement e, int[] sizes) { icoFile = pathname.NormalizeMinimally_(icoFile); using var stream = File.OpenWrite(icoFile); ConvertWpfImageElementToIcon_(stream, e, sizes); } /// /// Converts XAML icon to GDI+ icon. /// /// Icon name or XAML etc. See . /// /// If fails, prints warning and returns null. internal static System.Drawing.Icon XamlIconToGdipIcon_(string s, int size) { try { var e = ImageUtil.LoadWpfImageElement(s); var ms = new MemoryStream(); ImageUtil.ConvertWpfImageElementToIcon_(ms, e, [size]); ms.Position = 0; return new System.Drawing.Icon(ms); } catch (Exception ex) { print.warning(ex); return null; } } } ================================================ FILE: Au/Au.More/KeyToTextConverter.cs ================================================ namespace Au.More; /// /// Converts virtual-key codes to text characters. /// /// /// To record user input, can be used . /// When recording user input, use same KeyToTextConverter variable for all keys. /// public class KeyToTextConverter { (KKey vk, KMod mod, uint sc, nint hkl) _deadKey; /// /// Converts a virtual-key code to text. /// /// Receives text. Can be 1 character c, or string s with 2 or more characters. Receives default if this function returns false or if the key is a dead key. /// Virtual-key code. /// Scan code. /// Modifier keys (Shift etc). See . /// Thread id of the focused or active window. Need for keyboard layout. If 0, uses this thread. /// true if it's a text key or dead key. public unsafe bool Convert(out (char c, string s) text, KKey vk, uint sc, KMod mod, int threadId) { text = default; if (vk == KKey.Packet) { text.c = (char)sc; } else { if (!IsPossiblyChar_(mod, vk)) return false; var hkl = Api.GetKeyboardLayout(threadId); var ks = stackalloc byte[256]; _SetKS(mod); var c = stackalloc char[8]; bool win10 = osVersion.minWin10_1607; //the API resets dead key etc, but on new OS flag 4 prevents it int n = Api.ToUnicodeEx((uint)vk, sc, ks, c, 8, win10 ? 4u : 0u, hkl); if (n == 1 && c[0] < ' ') { Debug_.Print($"{(int)c[0]}, {c[0]}"); if (c[0] == '\r') c[n++] = '\n'; else if (c[0] is not ('\t' or '\n')) n = 0; } if (n == 1) text.c = c[0]; else if (n > 0) text.s = new(c, 0, n); if (!win10) { //if need, set dead key again if (_deadKey.vk != 0 && _deadKey.hkl == hkl) { _SetKS(_deadKey.mod); Api.ToUnicodeEx((uint)_deadKey.vk, _deadKey.sc, ks, c, 8, 0, hkl); _deadKey.vk = 0; } else if (n < 0) { _deadKey = (vk, mod, sc, hkl); Api.ToUnicodeEx((uint)vk, sc, ks, c, 8, 0, hkl); } } if (n == 0) return false; //non-char key void _SetKS(KMod m) { ks[(int)KKey.Shift] = (byte)((0 != (m & KMod.Shift)) ? 0x80 : 0); ks[(int)KKey.Ctrl] = (byte)((0 != (m & KMod.Ctrl)) ? 0x80 : 0); ks[(int)KKey.Alt] = (byte)((0 != (m & KMod.Alt)) ? 0x80 : 0); //ks[(int)KKey.Win] = (byte)((0 != (m & KMod.Win)) ? 0x80 : 0); ks[(int)KKey.CapsLock] = (byte)(keys.isCapsLock ? 1 : 0); //don't need this for num lock } } return true; //Notes: //1. Does not work with eg Chinese input method. //2. Catches everything that would later be changed by the app, or by a next hook, etc. //3. Don't know how to get Alt+numpad characters. Ignore them. // On Alt up could call tounicodeex with sc with flag 0x8000. It gets the char, but resets keyboard state, and the char is not typed. //4. In console windows does not work with Unicode characters. //if(MapVirtualKeyEx(vk, MAPVK_VK_TO_CHAR, hkl)&0x80000000) { print.it("DEAD"); return -1; } //this cannot be used because resets dead key } /// /// Clears internal fields such as dead key state. /// public void Clear() { _deadKey = default; } /// /// Returns true if the key + modifiers could generate a character, including Enter and Tab but not other control characters. /// internal static bool IsPossiblyChar_(KMod m, KKey k) { if (m.Has(KMod.Win) || (m & ~KMod.Shift) == KMod.Ctrl || m == (KMod.Alt | KMod.Shift)) return false; if (k is KKey.Back or KKey.Escape) return false; return true; } } ================================================ FILE: Au/Au.More/Math2.cs ================================================ namespace Au.More; /// /// Simple calculation functions. /// //[DebuggerStepThrough] public static class Math2 { /// /// Creates uint by placing (ushort)loWord in bits 1-16 and (ushort)hiWord in bits 17-32. /// Like C macro MAKELONG, MAKEWPARAM, MAKELPARAM, MAKELRESULT. /// /// The return value is of type nint. It can be used with Windows message API as lParam or wParam or return value. public static nint MakeLparam(int loWord, int hiWord) => MakeLparam((uint)loWord, (uint)hiWord); //Returns nint, because usually used as sendmessage etc parameter. If uint, would need to explicitly cast to nint. If somebody casts to int, the result may be incorrect, ie negative. //Why named MakeLparam, MakeWord, LoWord, HiWord: // 1. Like C macros MAKELPARAM/MAKEWORD/LOWORD/HIWORD. // 2. MakeLparam used mostly as lParam of sendmessage etc. /// public static nint MakeLparam(uint loWord, uint hiWord) => (nint)(((hiWord & 0xffff) << 16) | (loWord & 0xffff)); /// /// Creates uint by placing (ushort)p.x in bits 1-16 and (ushort)p.y in bits 17-32. /// Like C macro MAKELONG, MAKEWPARAM, MAKELPARAM, MAKELRESULT. /// /// The return value is of type nint. It can be used with Windows message API as lParam or wParam or return value. public static nint MakeLparam(POINT p) => MakeLparam((uint)p.x, (uint)p.y); /// /// Creates ushort by placing (byte)loByte in bits 1-8 and (byte)hiByte in bits 9-16. /// Like C macro MAKEWORD. /// public static ushort MakeWord(int loByte, int hiByte) => MakeWord((uint)loByte, (uint)hiByte); /// public static ushort MakeWord(uint loByte, uint hiByte) => (ushort)(((hiByte & 0xff) << 8) | (loByte & 0xff)); /// /// Gets bits 1-16 as ushort. /// Like C macro LOWORD. /// /// /// The parameter is interpreted as uint. The parameter type nint allows to avoid explicit cast from int and IntPtr. /// public static ushort LoWord(nint x) => (ushort)((uint)x & 0xFFFF); /// /// Gets bits 17-32 as ushort. /// Like C macro HIWORD. /// /// public static ushort HiWord(nint x) => (ushort)((uint)x >> 16); /// /// Gets bits 1-16 as short. /// Like C macro GET_X_LPARAM. /// /// public static short LoShort(nint x) => (short)((uint)x & 0xFFFF); /// /// Gets bits 17-32 as short. /// Like C macro GET_Y_LPARAM. /// /// public static short HiShort(nint x) => (short)((uint)x >> 16); /// /// Gets bits 1-8 as byte. /// Like C macro LOBYTE. /// public static byte LoByte(ushort x) => (byte)((uint)x & 0xFF); /// /// Gets bits 9-16 as byte. /// Like C macro HIBYTE. /// public static byte HiByte(ushort x) => (byte)((uint)x >> 8); /// /// Converts nint containing x and y coordinates to . /// public static POINT NintToPOINT(nint xy) => new(LoShort(xy), HiShort(xy)); /// /// Returns number * multiply / divide. /// Multiplies without overflow and rounds up or down to the nearest integer. /// /// /// public static int MulDiv(int number, int multiply, int divide) { if (divide == multiply) return number; long r = (long)number * multiply; int d = divide / 2; if (r < 0 == divide < 0) r += d; else r -= d; //round return checked((int)(r / divide)); //This code produces the same results as API MulDiv. Tested with millions of random and edge values. Faster. //The only difference, API does not support int.MinValue. } //public static int MulDiv(int number, int multiply, int divide) => Api.MulDiv(number, multiply, divide); /// /// Calculates how many % of whole is part: 100L * part / whole. /// /// /// /// Round down or up. If false (default), can only round down. /// public static int PercentFromValue(int whole, int part, bool canRoundUp = false) => whole == default ? default : (canRoundUp ? MulDiv(100, part, whole) : checked((int)(100L * part / whole))); /// /// Calculates how many % of whole is part: 100 * part / whole. /// public static double PercentFromValue(double whole, double part) => whole == default ? default : (100.0 * part / whole); /// /// Returns percent % of whole: (long)whole * percent / 100. /// /// /// /// Use , which can round down or up. If false (default), can only round down. /// public static int PercentToValue(int whole, int percent, bool canRoundUp = false) => canRoundUp ? MulDiv(whole, percent, 100) : checked((int)((long)whole * percent / 100L)); /// /// Returns percent % of whole: whole * percent / 100. /// public static double PercentToValue(double whole, double percent) => whole * percent / 100.0; /// /// If value is divisible by alignment, returns value. Else returns the nearest bigger number that is divisible by alignment. /// /// An integer value. /// Alignment. Must be a power of two (2, 4, 8, 16...). /// /// For example if alignment is 4, returns 4 if value is 1-4, returns 8 if value is 5-8, returns 12 if value is 9-10, and so on. /// /// /// /// public static int AlignUp(int value, uint alignment) => (int)AlignUp((uint)value, alignment); /// public static uint AlignUp(uint value, uint alignment) => (value + (alignment - 1)) & ~(alignment - 1); //shorter: (value + --alignment) & ~alignment. But possibly less optimized. Now (alignment - 1) and ~(alignment - 1) usually are constants. /// /// Swaps values of two variables: T t = a; a = b; b = t; /// public static void Swap(ref T a, ref T b) { T t = a; a = b; b = t; } /// /// Swaps two ranges of bits. /// /// /// Position of first range of bits. /// Position of second range of bits. /// Number of bits in each range. public static int SwapBits(int value, int i, int j, int n) => (int)SwapBits((uint)value, i, j, n); /// /// Swaps two ranges of bits. /// /// /// Position of first range of bits. /// Position of second range of bits. /// Number of bits in each range. public static uint SwapBits(uint value, int i, int j, int n) { // http://graphics.stanford.edu/~seander/bithacks.html#SwappingBitsXOR uint x = ((value >> i) ^ (value >> j)) & ((1U << n) - 1); // XOR temporary return value ^ ((x << i) | (x << j)); } //rejected. Too simple and does not save any code. Also would need generic, for enum too. ///// ///// Clears oldFlags bits specified in mask and adds newFlags bits specified in mask. ///// //int SetFlagsMasked(int oldFlags, int newFlags, int mask) => (oldFlags&~mask) | (newFlags&mask); /// /// Calculates angle degrees from coordinates x and y. /// public static double AngleFromXY(int x, int y) => Math.Atan2(y, x) * (180 / Math.PI); /// /// Calculates distance between two points. /// public static double Distance(POINT p1, POINT p2) { if (p1.y == p2.y) return Math.Abs(p2.x - p1.x); //horizontal line if (p1.x == p2.x) return Math.Abs(p2.y - p1.y); //vertical line long dx = p2.x - p1.x, dy = p2.y - p1.y; return Math.Sqrt(dx * dx + dy * dy); } /// /// Calculates distance between rectangle and point. /// /// If the point is outside, returns the nearest distance, else 0. public static double Distance(RECT r, POINT p) { r.Normalize(swap: true); if (r.Contains(p)) return 0; int x = p.x < r.left ? r.left : (p.x > r.right ? r.right : p.x); int y = p.y < r.top ? r.top : (p.y > r.bottom ? r.bottom : p.y); return Distance((x, y), p); } } ================================================ FILE: Au/Au.More/MemoryBitmap.cs ================================================ namespace Au.More; /// /// Creates and manages native bitmap handle and memory DC (GDI device context). /// The bitmap is selected in the DC. /// public class MemoryBitmap : IDisposable { IntPtr _dc, _bm, _oldbm; bool _disposed; /// /// DC handle. /// public IntPtr Hdc => _dc; /// /// Bitmap handle. /// public IntPtr Hbitmap => _bm; /// /// Does nothing. Later you can call or . /// public MemoryBitmap() { } /// /// Calls . /// /// width or height is less than 1. /// Failed. Probably there is not enough memory for bitmap of specified size (need with*height*4 bytes). public MemoryBitmap(int width, int height) { if (width <= 0 || height <= 0) throw new ArgumentException(); if (!Create(width, height)) throw new AuException("*create memory bitmap of specified size"); } //rejected: not obvious, whether it attaches or copies. Also, attaching is rarely used. ///// ///// Calls . ///// //public MemoryBitmap(IntPtr hBitmap) //{ // Attach(hBitmap); //} /// protected virtual void Dispose(bool disposing) { if (_disposed) return; _disposed = true; Delete(); } /// /// Deletes the bitmap and DC. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// ~MemoryBitmap() => Dispose(false); //Calls DeleteDC. MSDN says that ReleaseDC must be called from the same thread. But does not say it about DeleteDC and others. // Tested: DeleteDC returns true in finalizer (other thread). /// /// Deletes the bitmap and DC. /// public void Delete() { if (_dc == default) return; if (_bm != default) { Api.SelectObject(_dc, _oldbm); Api.DeleteObject(_bm); _bm = default; } Api.DeleteDC(_dc); _dc = default; } /// /// Creates new memory DC and bitmap of specified size and selects it into the DC. /// /// false if failed. In any case deletes previous bitmap and DC. /// Width, pixels. Must be > 0. /// Height, pixels. Must be > 0. public bool Create(int width, int height) { if (_disposed) throw new ObjectDisposedException(nameof(MemoryBitmap)); using var dcs = new ScreenDC_(); Attach(Api.CreateCompatibleBitmap(dcs, width, height)); return _bm != default; } /// /// Sets this variable to manage an existing bitmap. /// Selects the bitmap into a memory DC. /// Deletes previous bitmap and DC. /// /// Native bitmap handle. public void Attach(IntPtr hBitmap) { if (_disposed) throw new ObjectDisposedException(nameof(MemoryBitmap)); Delete(); if (hBitmap != default) { _dc = Api.CreateCompatibleDC(default); _oldbm = Api.SelectObject(_dc, _bm = hBitmap); } } /// /// Deletes memory DC, clears this variable and returns its bitmap (native bitmap handle). /// The returned bitmap is not selected into a DC. Will need to delete it with API DeleteObject. /// public IntPtr Detach() { IntPtr bret = _bm; if (_bm != default) { Api.SelectObject(_dc, _oldbm); Api.DeleteDC(_dc); _dc = default; _bm = default; } return bret; } } ================================================ FILE: Au/Au.More/MemoryUtil.cs ================================================ namespace Au.More; /// /// Allocates memory from native heap of this process using heap API. /// Also has more functions to work with memory: copy, move, virtual alloc. /// /// /// Uses the common heap of this process, API GetProcessHeap. /// public static unsafe class MemoryUtil { static IntPtr _processHeap = Api.GetProcessHeap(); /// /// Allocates new memory block and returns its address. /// /// Byte count. /// Set all bytes = 0. /// Failed. Probably size is too big. /// /// Calls API HeapAlloc. /// The memory is unmanaged and will not be freed automatically. Always call when done. Call or if need to resize. /// public static byte* Alloc(nint size, bool zeroInit = false) => _ReAllocBytes(null, size, zeroInit); /// Count of elements of type T. /// public static T* Alloc(nint count, bool zeroInit = false) where T : unmanaged => (T*)_ReAllocBytes(null, count * sizeof(T), zeroInit); //Rejected. With the above overload the calling code is easier to read. Not so often used. //public static void Alloc(out T* mem, nint count, bool zeroInit = false) where T : unmanaged // => mem = (T*)_ReAllocBytes(null, count * sizeof(T), zeroInit); static byte* _ReAllocBytes(void* mem, nint size, bool zeroInit = false) { uint flag = zeroInit ? 8u : 0u; for (int i = 0; i < 5; i++) { if (i > 0) { GC.Collect(); GC.WaitForPendingFinalizers(); Thread.Sleep(i * 100); } void* r; if (mem == null) r = Api.HeapAlloc(_processHeap, flag, size); else r = Api.HeapReAlloc(_processHeap, flag, mem, size); if (r != null) return (byte*)r; } throw new OutOfMemoryException(); //note: don't need GC.AddMemoryPressure. // Native memory usually is used for temporary buffers etc and is soon released eg with try/finally. // Marshal.AllocHGlobal does not do it too. } /// /// Reallocates a memory block to make it bigger or smaller. /// /// Input: old memory address; if null, allocates new memory like . Output: new memory address. Unchanged if exception. /// New count of elements of type T. /// When size is growing, set all added bytes = 0. /// Failed. Probably count is too big. /// /// Calls API HeapReAlloc or HeapAlloc. /// Preserves data in Math.Min(oldCount, newCount) elements of old memory (copies from old memory if need). /// The memory is unmanaged and will not be freed automatically. Always call when done. Call ReAlloc or if need to resize. /// public static void ReAlloc(ref T* mem, nint count, bool zeroInit = false) where T : unmanaged => mem = (T*)_ReAllocBytes(mem, count * sizeof(T), zeroInit); /// /// Frees a memory block. /// Does nothing if mem is null. /// /// /// Calls API HeapFree. /// public static void Free(void* mem) { if (mem != null) Api.HeapFree(_processHeap, 0, mem); } /// /// Frees a memory block (if not null) and allocates new. /// /// Input: old memory address or null. Output: new memory address; null if exception (it prevents freeing twice). /// New count of elements of type T. /// Set all bytes = 0. /// Failed. Probably count is too big. /// /// At first sets mem = null, to avoid double if this function throws exception. Then calls and . /// public static void FreeAlloc(ref T* mem, nint count, bool zeroInit = false) where T : unmanaged { var m = mem; mem = null; Free(m); mem = Alloc(count, zeroInit); } /// /// Allocates new virtual memory block with API VirtualAlloc and returns its address: VirtualAlloc(default, size, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE). /// /// Byte count. /// Failed. Probably size is too big. /// /// Faster than managed and when memory size is large, more than 1 MB; else slower. /// The memory is initialized to zero (all bytes 0). /// public static byte* VirtualAlloc(nint size) { for (int i = 0; i < 5; i++) { if (i > 0) { GC.Collect(); GC.WaitForPendingFinalizers(); Thread.Sleep(i * 100); } var r = (byte*)Api.VirtualAlloc(default, size, Api.MEM_COMMIT | Api.MEM_RESERVE, Api.PAGE_READWRITE); if (r != null) return r; } throw new OutOfMemoryException(); //note: don't need GC.AddMemoryPressure. // Native memory usually is used for temporary buffers etc and is soon released eg with try/finally. // Marshal.AllocHGlobal does not do it too. } /// /// Frees a memory block allocated with . /// Does nothing if mem is null. /// public static void VirtualFree(void* mem) { if (mem != null) Api.VirtualFree(mem); } /// /// Copies memory with . /// /// /// If some part of memory blocks overlaps, this function is much slower than . Else same speed or slightly faster. /// public static void Copy(void* from, void* to, nint size) => Buffer.MemoryCopy(from, to, size, size); //speed Buffer.MemoryCopy vs memcpy, non-overlapped: same if small, slightly faster if big. //speed Span.CopyTo vs Buffer.MemoryCopy: same if non-overlapped, slower if overlapped. /// /// Copies memory with API memmove. /// /// /// If some part of memory blocks overlaps, this function is much faster than . Else same speed or slightly slower. /// public static void Move(void* from, void* to, nint size) => Api.memmove(to, from, size); } ================================================ FILE: Au/Au.More/MenuItemInfo.cs ================================================ namespace Au.More { /// /// Gets item id, text and other info of a classic menu. /// public class MenuItemInfo { IntPtr _hm; int _id; bool _isSystem; wnd _ow; private MenuItemInfo() { } /// /// Gets info of a menu item from point. /// /// null if failed, eg the point is not in the menu or the window is hung. /// Point in screen coordinates. /// Popup menu window, class name "#32768". /// Timeout (ms) to use when the window is busy or hung. public static MenuItemInfo FromXY(POINT pScreen, wnd w, int msTimeout = 5000) { if (!w.SendTimeout(msTimeout, out var hm, Api.MN_GETHMENU)) return null; int i = Api.MenuItemFromPoint(default, hm, pScreen); if (i == -1) return null; i = Api.GetMenuItemID(hm, i); if (i == -1 || i == 0) return null; return new MenuItemInfo { _hm = hm, _id = i }; } /// /// Gets info of a menu item from mouse. /// /// null if failed, eg the point is not in a menu or the window is hung. /// Timeout (ms) to use when the window is busy or hung. public static MenuItemInfo FromXY(int msTimeout = 5000) { var p = mouse.xy; var w = wnd.fromXY(p, WXYFlags.Raw); if (!w.ClassNameIs("#32768")) return null; return FromXY(p, w, msTimeout); } /// /// Gets the popup menu handle. /// public IntPtr MenuHandle => _hm; /// /// Gets menu item id. /// public int ItemId => _id; /// /// Gets the owner window of the popup menu. /// public wnd OwnerWindow => _OwnerSystem().ow; /// /// true if it is a system menu, eg when right-clicked the title bar of a window. /// public bool IsSystem => _OwnerSystem().sys; (wnd ow, bool sys) _OwnerSystem() { if (_ow.Is0 && miscInfo.getGUIThreadInfo(out var g)) { _ow = g.hwndMenuOwner; _isSystem = g.flags.Has(GTIFlags.SYSTEMMENUMODE); } return (_ow, _isSystem); } /// /// Gets menu item text. /// /// null if failed. /// If contains '\t' character, get substring before it. /// Call . public string GetText(bool removeHotkey, bool removeAmp) => GetText(_hm, _id, false, removeHotkey, removeAmp); /// /// Gets menu item text. /// /// null if failed. /// /// /// id is 0-based index. For example you can use it to get text of a submenu-item, because such items usually don't have id. /// If contains '\t' character, get substring before it. /// Call . [SkipLocalsInit] public static unsafe string GetText(IntPtr menuHandle, int id, bool byIndex, bool removeHotkey, bool removeAmp) { var mi = new Api.MENUITEMINFO(Api.MIIM_STRING); if (!Api.GetMenuItemInfo(menuHandle, id, byIndex, ref mi)) return null; //get required buffer size if (mi.cch == 0) return ""; using FastBuffer b = new(mi.cch + 1); mi.cch = b.n; mi.dwTypeData = b.p; if (!Api.GetMenuItemInfo(menuHandle, id, byIndex, ref mi)) return null; var s = b.GetStringFindLength(); if (removeHotkey) { int i = s.IndexOf('\t'); if (i >= 0) s = s[..i]; } if (removeAmp) s = StringUtil.RemoveUnderlineChar(s); return s; } } } ================================================ FILE: Au/Au.More/MouseCursor.cs ================================================ namespace Au.More; /// /// Helps to load cursors, etc. Contains native cursor handle. /// /// /// To load cursors for winforms can be used constructors, but they don't support colors, ani cursors and custom size. /// Don't use this class to load cursors for WPF. Its Cursor class loads cursors correctly. /// public class MouseCursor { IntPtr _handle; //rejected: use HandleCollector like with icon. Unlikely somebody will use many cursors or even find this class. /// /// Sets native cursor handle. /// The cursor will be destroyed when disposing this variable or when converting to object of other type. /// public MouseCursor(IntPtr hcursor) { _handle = hcursor; } /// /// Destroys native cursor handle. /// public void Dispose() { if (_handle != default) { Api.DestroyIcon(_handle); _handle = default; } } /// ~MouseCursor() => Dispose(); /// /// Gets native cursor handle. /// public IntPtr Handle => _handle; ///// ///// Gets native cursor handle. ///// //public static implicit operator IntPtr(MouseCursor cursor) => cursor._handle; /// /// Loads cursor from file. /// /// default(MouseCursor) if failed. /// .cur or .ani file. If not full path, uses . /// Width and height. If 0, uses system default size, which depends on DPI. public static MouseCursor Load(string file, int size = 0) { file = pathname.normalize(file, folders.ThisAppImages); if (file == null) return null; uint fl = Api.LR_LOADFROMFILE; if (size == 0) fl |= Api.LR_DEFAULTSIZE; return new MouseCursor(Api.LoadImage(default, file, Api.IMAGE_CURSOR, size, size, fl)); } /// /// Creates cursor from cursor file data in memory, for example from a managed resource. /// /// default(MouseCursor) if failed. /// Data of .cur or .ani file. /// Width and height. If 0, uses system default size, which depends on DPI. /// /// This function creates/deletes a temporary file, because there is no good API to load cursor from memory. /// public static MouseCursor Load(byte[] cursorData, int size = 0) { using var tf = new TempFile(); File.WriteAllBytes(tf, cursorData); return Load(tf, size); //If want to avoid temp file, can use: // 1. CreateIconFromResourceEx. // But quite much unsafe code (at first need to find cursor offset (of size) and set hotspot), less reliable (may fail for some .ani files), in some cases works not as well (may get wrong-size cursor). // The code moved to the Unused project. // 2. CreateIconIndirect. Not tested. No ani. Need to find cursor offset (of size). //WPF Cursor ctor uses temp file too. } /// /// Creates object that shares native cursor handle with this object. /// /// null if is default(IntPtr). public System.Windows.Forms.Cursor ToGdipCursor() { if (_handle == default) return null; var R = new System.Windows.Forms.Cursor(_handle); s_cwt.Add(R, this); return R; } static readonly ConditionalWeakTable s_cwt = new(); //rejected. Don't need. WPF can load from file or stream. Loads correctly. ///// ///// Creates WPF Cursor object from this native cursor. ///// ///// null if Handle is default(IntPtr). ///// If true (default), the returned variable owns the unmanaged cursor and destroys it when disposing. If false, the returned variable just uses the cursor handle and will not destroy; later will need to dispose this variable. //public System.Windows.Input.Cursor ToWpfCursor(bool destroyCursor = true) { // if (_handle == default) return null; // var R = System.Windows.Interop.CursorInteropHelper.Create(new _CursorHandle(_handle, destroyCursor)); // if (destroyCursor) _handle = default; // return R; //} //class _CursorHandle : SafeHandleZeroOrMinusOneIsInvalid //{ // public _CursorHandle(IntPtr handle, bool ownsHandle) : base(ownsHandle) { base.handle = handle; } // protected override bool ReleaseHandle() => Api.DestroyCursor(handle); //} /// /// Calculates 64-bit FNV1 hash of cursor's mask bitmap. /// /// 0 if failed. public static unsafe long Hash(IntPtr hCursor) { using Api.ICONINFO ii = new(hCursor); var hb = Api.CopyImage(ii.hbmMask, Api.IMAGE_BITMAP, 0, 0, Api.LR_COPYDELETEORG | Api.LR_CREATEDIBSECTION); long R = 0; Api.BITMAP b; if (0 != Api.GetObject(hb, sizeof(Api.BITMAP), &b) && b.bmBits != default) R = More.Hash.Fnv1Long((byte*)b.bmBits, b.bmHeight * b.bmWidthBytes); return R; } ///// ///// Calculates 64-bit FNV1 hash of cursor's mask bitmap. ///// ///// 0 if failed. //public unsafe long Hash() => Hash(_handle); /// /// Gets current global mouse cursor. /// /// false if cursor is hidden. /// Receives native handle. Don't destroy. public static bool GetCurrentVisibleCursor(out IntPtr cursor) { Api.CURSORINFO ci = default; ci.cbSize = Api.SizeOf(ci); if (Api.GetCursorInfo(ref ci) && ci.hCursor != default && 0 != (ci.flags & Api.CURSOR_SHOWING)) { cursor = ci.hCursor; return true; } cursor = default; return false; } /// /// Workaround for brief "wait" cursor when mouse enters a window of current thread first time. /// Reason: default thread's cursor is "wait". OS shows it before the first WM_SETCURSOR sets correct cursor. /// Call eg on WM_CREATE. /// internal static void SetArrowCursor_() { var h = Api.LoadCursor(default, MCursor.Arrow); if (Api.GetCursor() != h) Api.SetCursor(h); } } ================================================ FILE: Au/Au.More/RecordingUtil.cs ================================================ namespace Au.More { /// /// Functions for keyboard/mouse/etc recorder tools. /// public static partial class RecordingUtil { /// /// Converts multiple recorded mouse movements to string for . /// /// /// List of x y distances from previous. /// The first distance is from mouse position before the first movement; at run time it will be distance from . /// To create uint value from distance dx dy use and cast to uint. /// /// /// recorded also contains sleep times (milliseconds) alternating with distances. /// It must start with a sleep time. Example: {time1, dist1, time2, dist2}. Another example: {time1, dist1, time2, dist2, time3}. This is invalid: {dist1, time1, dist2, time2}. /// public static string MouseToString(IEnumerable recorded, bool withSleepTimes) { var a = new List(); byte flags = 0; if (withSleepTimes) flags |= 1; a.Add(flags); int pdx = 0, pdy = 0; bool isSleep = withSleepTimes; foreach (var u in recorded) { int v, nbytes = 4; if (isSleep) { v = (int)Math.Min(u, 0x3fffffff); if (v > 3) v--; //_SendMove usually takes 0.5-1.5 ms if (v <= 1 << 6) nbytes = 1; else if (v <= 1 << 14) nbytes = 2; else if (v <= 1 << 22) nbytes = 3; //print.it($"nbytes={nbytes} sleep={v}"); //never mind: ~90% is 7. Removing it would make almost 2 times smaller string. But need much more code. Or compress (see comment below). } else { //info: to make more compact, we write not distances (dx dy) but distance changes (x y). int dx = Math2.LoShort((nint)u), x = dx - pdx; pdx = dx; int dy = Math2.HiShort((nint)u), y = dy - pdy; pdy = dy; if (x >= -4 && x < 4 && y >= -4 && y < 4) nbytes = 1; //3+3+2=8 bits, 90% else if (x >= -64 && x < 64 && y >= -64 && y < 64) nbytes = 2; //7+7+2=16 bits, ~10% else if (x >= -1024 && x < 1024 && y >= -1024 && y < 1024) nbytes = 3; //11+11+2=24 bits, ~0% int shift = nbytes * 4 - 1, mask = (1 << shift) - 1; v = (x & mask) | ((y & mask) << shift); //print.it($"dx={dx} dy={dy} x={x} y={y} nbytes={nbytes} v=0x{v:X}"); } v <<= 2; v |= (nbytes - 1); for (; nbytes != 0; nbytes--, v >>= 8) a.Add((byte)v); isSleep ^= withSleepTimes; } //rejected: by default compresses to ~80% (20% smaller). When withSleepTimes, to ~50%, but never mind, rarely used. //print.it(a.Count, Convert2.Compress(a.ToArray()).Length); return Convert.ToBase64String(a.ToArray()); } } } ================================================ FILE: Au/Au.More/ResourceUtil.cs ================================================ using System.Resources; using System.Windows.Media.Imaging; using System.Windows.Markup; using System.Windows; using System.Windows.Controls; namespace Au.More; /// /// Gets managed resources from a .NET assembly. /// /// /// Internally uses . Uses . /// /// Loads resources from managed resource "AssemblyName.g.resources". To add such resource files in Visual Studio, set file build action = Resource. Don't use .resx files and the Resources page in Project Properties. /// /// By default loads resources from the app entry assembly. In script with role miniProgram - from the script's assembly. To specify another loaded assembly, use prefix like "<AssemblyName>" or "*<AssemblyName>". /// /// The resource name argument can optionally start with "resource:". /// /// Does not use caching. Creates new object even when loading the resource not the first time. /// public static class ResourceUtil { /// /// Gets resource of any type. /// /// Resource name, like "file.txt" or "sub/file.txt". More info: . /// Cannot find assembly or resource. /// The resource is of different type. This function does not convert. /// Other exceptions that may be thrown by used .NET functions. public static T Get(string name) { var o = _GetObject(ref name); if (o is T r) return r; throw new InvalidOperationException($"Resource '{name}' is not {typeof(T).Name}; it is {o.GetType().Name}."); } /// /// Gets stream. /// /// Resource name, like "file.png" or "sub/file.png". More info: . /// Cannot find assembly or resource. /// The resource type is not stream. /// Other exceptions that may be thrown by used .NET functions. /// /// Don't need to dispose the stream. /// public static UnmanagedMemoryStream GetStream(string name) { //if (name.Starts("pack:")) return _Pack(name); //rejected return Get(name); } /// /// Gets string. /// /// Resource name, like "myString" or "file.txt" or "sub/file.txt". More info: . /// Cannot find assembly or resource. /// Unsupported resource type. /// Other exceptions that may be thrown by used .NET functions. /// /// Supports resources of type string, byte[] (UTF-8), stream (UTF-8). /// public static string GetString(string name) { var o = _GetObject(ref name); switch (o) { case string s: return s; case byte[] a: return Encoding.UTF8.GetString(a); case UnmanagedMemoryStream m: return new StreamReader(m, Encoding.UTF8).ReadToEnd(); } throw new InvalidOperationException($"Resource '{name}' is not string, byte[] or stream; it is {o.GetType().Name}."); } internal static string TryGetString_(string name) => _TryGetObject(ref name) as string; /// /// Gets byte[]. /// /// Resource name, like "file.txt" or "sub/file.txt". More info: . /// Cannot find assembly or resource. /// Unsupported resource type. /// Other exceptions that may be thrown by used .NET functions. /// /// Supports resources of type byte[], string (gets UTF-8 bytes), stream. /// public static byte[] GetBytes(string name) { var o = _GetObject(ref name); switch (o) { case byte[] a: return a; case string s: return Encoding.UTF8.GetBytes(s); case UnmanagedMemoryStream m: var b = new byte[m.Length]; m.Read(b); return b; } throw new InvalidOperationException($"Resource '{name}' is not byte[], string or stream; it is {o.GetType().Name}."); } /// /// Gets GDI+ image. /// /// Resource name, like "file.png" or "sub/file.png". More info: . /// Cannot find assembly or resource. /// The resource type is not stream. /// Other exceptions that may be thrown by used .NET functions. public static System.Drawing.Bitmap GetGdipBitmap(string name) { return new System.Drawing.Bitmap(GetStream(name)); } //rejected. Too simple and rare. ///// ///// Gets GDI+ icon. ///// ///// Resource name, like "file.ico" or "sub/file.ico". More info: . ///// Cannot find assembly or resource. ///// The resource type is not stream. ///// Other exceptions that may be thrown by used .NET functions. //public static System.Drawing.Icon GetGdipIcon(string name) { // return new System.Drawing.Icon(GetStream(name)); //} /// /// Gets WPF image or icon that can be used as ImageSource. /// /// Resource name, like "file.png" or "sub/file.png". More info: . /// Cannot find assembly or resource. /// The resource type is not stream. /// Other exceptions that may be thrown by used .NET functions. public static BitmapFrame GetWpfImage(string name) { return BitmapFrame.Create(GetStream(name)); } /// /// Gets WPF object from XAML resource, for example image. /// /// An object of type of the XAML root object, for example if . /// Resource name, like "file.xaml" or "sub/file.xaml". More info: . /// Cannot find assembly or resource. /// The resource type is not stream. /// Other exceptions that may be thrown by used .NET functions. public static object GetXamlObject(string name) { return XamlReader.Load(GetStream(name)); } /// /// Gets WPF image element from XAML or other image resource. /// /// Resource name, like "file.png" or "sub/file.xaml". More info: . /// Cannot find assembly or resource. /// The resource type is not stream. /// Other exceptions that may be thrown by used .NET functions. /// /// If name ends with ".xaml" (case-insensitive), calls . Else returns with Source = . /// public static FrameworkElement GetWpfImageElement(string name) { if (name.Ends(".xaml", true)) return (FrameworkElement)GetXamlObject(name); return new Image { Source = GetWpfImage(name) }; } //probably not useful ///// ///// Gets WPF image as BitmapImage. ///// ///// Resource name, like "file.png" or "sub/file.png". More info: . ///// Cannot find assembly or resource. ///// The resource type is not stream. ///// Other exceptions that may be thrown by used .NET functions. //public static BitmapImage GetWpfBitmapImage(string name) { // var st = GetStream(name); // var bi = new BitmapImage(); // bi.BeginInit(); // bi.CacheOption = BitmapCacheOption.OnLoad; // bi.StreamSource = st; // bi.EndInit(); // return bi; //} /// /// Returns true if string starts with "resource:" or "resources/". /// public static bool HasResourcePrefix(string s) { return s.Starts("resource:") || s.Starts("resources/")/* || s.Starts("pack:")*/; } //[MethodImpl(MethodImplOptions.NoInlining)] //avoid loading WPF dlls if no "pack:" //static UnmanagedMemoryStream _Pack(string name) { // if (script.role == SRole.MiniProgram && !name.Contains(";component/") && name.Starts("pack://application:,,,/")) name = name.Insert(23, script.name + ";component/"); // if (Application.Current == null) new Application(); // return Application.GetResourceStream(new Uri(name)).Stream as UnmanagedMemoryStream; //} static object _GetObject(ref string name) => _TryGetObject(ref name) ?? throw new FileNotFoundException($"Cannot find resource '{name}'."); static object _TryGetObject(ref string name) { var rs = _RS(ref name, true); if (rs == null) return null; var r = rs.GetObject(name); if (r == null) r = rs.GetObject(name.Lower()); return r; } static ResourceSet _RS(ref string name, bool noThrow = false) { if (name.Starts("resource:")) name = name[9..]; string asmName = ""; if (name is ['<', ..] or ['*', '<', ..]) { int i = name[0] == '*' ? 2 : 1; int j = name.IndexOf('>', i); if (j >= i) { asmName = name[i..j]; name = name[++j..]; } } lock (s_dict) { if (!s_dict.TryGetValue(asmName, out var rs)) { var asm = asmName.Length == 0 ? AssemblyUtil_.GetEntryAssembly() : _FindAssembly(asmName); if (asm == null) return noThrow ? null : throw new FileNotFoundException($"Cannot find loaded resource assembly '{asmName}'."); var rm = new ResourceManager(asm.GetName().Name + ".g", asm); rs = rm.GetResourceSet(CultureInfo.InvariantCulture, true, false); if (rs == null) return noThrow ? null : throw new FileNotFoundException($"Cannot find resources in assembly '{asmName}'."); s_dict.Add(asmName, rs); } return rs; } } static readonly Dictionary s_dict = new(StringComparer.OrdinalIgnoreCase); static Assembly _FindAssembly(string name) { foreach (var v in AppDomain.CurrentDomain.GetAssemblies()) if (v.GetName().Name.Eqi(name)) return v; return null; } } ================================================ FILE: Au/Au.More/SecurityUtil.cs ================================================ namespace Au.More { /// /// Security-related functions, such as enabling privileges. /// public static class SecurityUtil { /// /// Enables or disables a privilege for this process. /// /// false if failed. Supports . public static bool SetPrivilege(string privilegeName, bool enable, string computer = null) { bool ok = false; var p = new Api.TOKEN_PRIVILEGES { PrivilegeCount = 1, Privileges = new Api.LUID_AND_ATTRIBUTES { Attributes = enable ? 2u : 0 } }; //SE_PRIVILEGE_ENABLED if (Api.LookupPrivilegeValue(computer, privilegeName, out p.Privileges.Luid)) { Api.OpenProcessToken(Api.GetCurrentProcess(), Api.TOKEN_ADJUST_PRIVILEGES, out Handle_ hToken); Api.AdjustTokenPrivileges(hToken, false, p, 0, null, default); ok = 0 == lastError.code; hToken.Dispose(); } return ok; } } } ================================================ FILE: Au/Au.More/WaitableTimer.cs ================================================ namespace Au.More; /// /// Wraps a waitable timer handle. /// /// /// More info: API CreateWaitableTimer. /// public class WaitableTimer : WaitHandle { WaitableTimer(IntPtr h) => SafeWaitHandle = new Microsoft.Win32.SafeHandles.SafeWaitHandle(h, true); /// /// Calls API CreateWaitableTimer and creates a object that wraps the timer handle. /// /// /// Timer name. If a timer with this name already exists, opens it if possible. If null, creates unnamed timer. /// Failed. For example, a non-timer kernel object with this name already exists. public static WaitableTimer Create(bool manualReset = false, string timerName = null) { var h = Api.CreateWaitableTimer(Api.SECURITY_ATTRIBUTES.ForLowIL, manualReset, timerName); if (h.Is0) throw new AuException(0, "*create timer"); return new WaitableTimer(h); } /// /// Calls API OpenWaitableTimer and creates a object that wraps the timer handle. /// /// Timer name. Fails if it does not exist; to open-or-create use . /// See Synchronization Object Security and Access Rights. The default value TIMER_MODIFY_STATE|SYNCHRONIZE allows to set and wait. /// /// If fails, return null, don't throw exception. Supports . /// Failed. For example, the timer does not exist. public static WaitableTimer Open(string timerName, uint access = Api.TIMER_MODIFY_STATE | Api.SYNCHRONIZE, bool inheritHandle = false, bool noException = false) { var h = Api.OpenWaitableTimer(access, inheritHandle, timerName); if (h.Is0) { var e = lastError.code; if (noException) { lastError.code = e; return null; } throw new AuException(e, "*open timer"); } return new WaitableTimer(h); } /// /// Calls API SetWaitableTimer. /// /// false if failed. Supports . /// /// The time after which the state of the timer is to be set to signaled. It is relative time (from now). /// If positive, in milliseconds. If negative, in 100 nanosecond intervals (microseconds * 10), see FILETIME. /// Also can be 0, to set minimal time. /// The period of the timer, in milliseconds. If 0, the timer is signaled once. If greater than 0, the timer is periodic. /// dueTime*10000 is greater than . public bool Set(long dueTime, int period = 0) { if (dueTime > 0) dueTime = -checked(dueTime * 10000); return Api.SetWaitableTimer(this.SafeWaitHandle.DangerousGetHandle(), ref dueTime, period, default, default, false); } /// /// Calls API SetWaitableTimer. /// /// false if failed. Supports . /// The UTC date/time at which the state of the timer is to be set to signaled. /// The period of the timer, in milliseconds. If 0, the timer is signaled once. If greater than 0, the timer is periodic. public bool SetAbsolute(DateTime dueTime, int period = 0) { var t = dueTime.ToFileTimeUtc(); return Api.SetWaitableTimer(this.SafeWaitHandle.DangerousGetHandle(), ref t, period, default, default, false); } } ================================================ FILE: Au/Au.More/WinEventHook.cs ================================================ namespace Au.More { /// /// Helps with UI element event hooks. See API SetWinEventHook. /// /// /// The thread that uses hooks must process Windows messages. For example have a window/dialog/messagebox, or use a "wait-for" function that dispatches messages or has such option (see ). /// /// The variable should be disposed when don't need, or at least unhooked, either explicitly (call or in same thread) or with using. Can do it in hook procedure. /// /// /// { /// print.it(x.event_, x.w); /// var e = x.GetElm(); /// print.it(e); /// if(x.w.ClassNameIs("Shell_TrayWnd")) stop = true; /// }); /// dialog.show("hook"); /// //or /// //wait.doEventsUntil(-10, () => stop); //wait max 10 s for activated taskbar /// //print.it("the end"); /// ]]> /// [DebuggerStepThrough] public sealed class WinEventHook : IDisposable { IntPtr[] _a; Api.WINEVENTPROC _proc1; //our intermediate hook proc that calls _proc2 Action _proc2; //caller's hook proc [ThreadStatic] static List t_antiGC; /// /// Sets a hook for an event or a range of events. /// /// The lowest event constant value in the range of events. Can be to indicate the lowest possible event value. Events reference: SetWinEventHook. Value 0 is ignored. /// The highest event constant value in the range of events. Can be to indicate the highest possible event value. If 0, uses eventMin. /// The hook procedure (function that handles hook events). /// The id of the process from which the hook function receives events. If 0 - all processes on the current desktop. /// The native id of the thread from which the hook function receives events. If 0 - all threads. /// /// Failed. /// See . public WinEventHook(EEvent eventMin, EEvent eventMax, Action hookProc, int idProcess = 0, int idThread = 0, EHookFlags flags = 0) { Not_.Null(hookProc); _proc1 = _HookProc; Hook(eventMin, eventMax, idProcess, idThread, flags); _proc2 = hookProc; (t_antiGC ??= new()).Add(this); } /// /// Sets multiple hooks. /// /// Events. Reference: API SetWinEventHook. Elements with value 0 are ignored. /// public WinEventHook(EEvent[] events, Action hookProc, int idProcess = 0, int idThread = 0, EHookFlags flags = 0) { Not_.Null(hookProc); _proc1 = _HookProc; Hook(events, idProcess, idThread, flags); _proc2 = hookProc; (t_antiGC ??= new()).Add(this); } /// Hooks are already set and not called. /// public void Hook(EEvent eventMin, EEvent eventMax = 0, int idProcess = 0, int idThread = 0, EHookFlags flags = 0) { _Throw1(); _a = new IntPtr[1]; _SetHook(0, eventMin, eventMax, idProcess, idThread, flags); } /// Hooks are already set and not called. /// public void Hook(EEvent[] events, int idProcess = 0, int idThread = 0, EHookFlags flags = 0) { _Throw1(); _a = new IntPtr[events.Length]; for (int i = 0; i < events.Length; i++) _SetHook(i, events[i], 0, idProcess, idThread, flags); } void _SetHook(int i, EEvent eMin, EEvent eMax, int idProcess, int idThread, EHookFlags flags) { if (eMin == 0) return; if (eMax == 0) eMax = eMin; var hh = Api.SetWinEventHook(eMin, eMax, default, _proc1, idProcess, idThread, flags); if (hh == default) { var ec = lastError.code; Unhook(); throw new AuException(ec, "*set hook for " + eMin.ToString()); } _a[i] = hh; } void _Throw1() { if (_a != null) throw new InvalidOperationException(); if (_proc1 == null) throw new ObjectDisposedException(nameof(WinEventHook)); } /// /// Adds a hook for an event or a range of events. /// /// An int value greater than 0 that can be used with . /// /// Parameters are the same as of the constructor, but values can be different. /// /// This function together with can be used to temporarily add/remove one or more hooks while using the same variable and hook procedure. Don't need to call before. /// /// public int Add(EEvent eventMin, EEvent eventMax = 0, int idProcess = 0, int idThread = 0, EHookFlags flags = 0) { if (_proc1 == null) throw new ObjectDisposedException(nameof(WinEventHook)); int i = 0; if (_a == null) { _a = new IntPtr[1]; } else { for (; i < _a.Length; i++) if (_a[i] == default) goto g1; Array.Resize(ref _a, i + 1); } g1: _SetHook(i, eventMin, eventMax, idProcess, idThread, flags); return i + 1; } /// /// Removes a hook added by . /// /// A return value of . /// public void Remove(int addedId) { addedId--; if (_a == null || (uint)addedId >= _a.Length || _a[addedId] == default) throw new ArgumentException(); if (!Api.UnhookWinEvent(_a[addedId])) print.warning("Failed to unhook WinEventHook."); _a[addedId] = default; } ///// ///// True if hooks are set. ///// //public bool Installed => _a != null; /// /// Removes all hooks. /// /// /// Does nothing if already removed or wasn't set. /// Must be called from the same thread that sets the hook. /// public void Unhook() { if (_a != null) { foreach (var hh in _a) { if (hh == default) continue; if (!Api.UnhookWinEvent(hh)) print.warning("WinEventHook.Unhook() failed."); } _a = null; } } /// /// Calls . /// public void Dispose() { Unhook(); _proc1 = null; t_antiGC.Remove(this); GC.SuppressFinalize(this); } /// /// Prints a warning if the variable is not disposed. Cannot dispose in finalizer. /// ~WinEventHook() { //MSDN: UnhookWinEvent fails if called from a thread different from the call that corresponds to SetWinEventHook. if (_a != null) print.warning("Non-disposed WinEventHook variable."); } void _HookProc(IntPtr hHook, EEvent ev, wnd w, EObjid idObject, int idChild, int thread, int time) { try { _proc2(new HookData.WinEvent(this, ev, w, idObject, idChild, thread, time)); } catch (Exception ex) { WindowsHook.OnException_(ex); } } } } namespace Au.Types { public static partial class HookData { /// /// Hook data for the hook procedure set by . /// More info: API WinEventProc. /// public unsafe struct WinEvent { /// The caller object of your hook procedure. For example can be used to unhook. public readonly WinEventHook hook; /// API WinEventProc public readonly EEvent event_; /// API WinEventProc public readonly wnd w; /// API WinEventProc public readonly EObjid idObject; /// API WinEventProc public readonly int idChild; /// API WinEventProc public readonly int thread; /// API WinEventProc public readonly int time; internal WinEvent(WinEventHook hook, EEvent event_, wnd hwnd, EObjid idObject, int idChild, int thread, int time) { this.hook = hook; this.event_ = event_; this.w = hwnd; this.idObject = idObject; this.idChild = idChild; this.thread = thread; this.time = time; } /// /// Calls . /// public elm GetElm() { return elm.fromEvent(w, idObject, idChild); } } } } ================================================ FILE: Au/Au.More/WindowsHook.cs ================================================ namespace Au.More { /// /// Wraps API SetWindowsHookEx. /// /// /// Hooks are used to receive notifications about various system events. Keyboard and mouse input, window messages, various window events. /// /// Threads that use hooks must process Windows messages. For example have a window/dialog/messagebox, or use a "wait-for" function that dispatches messages or has such option (see ). /// /// The variable should be disposed when don't need, or at least unhooked, either explicitly (call or in same thread) or with using. Can do it in hook procedure. /// /// Avoid many hooks. Each low-level keyboard or mouse hook makes the computer slower, even if the hook procedure is fast. On each input event (key down, key up, mouse move, click, wheel) Windows sends a message to your thread. /// /// To receive hook events is used a callback function, aka hook procedure. Hook procedures of some hook types can block some events (call or return true). Blocked events are not sent to apps and older hooks. /// /// Delegates of hook procedures are protected from GC until called or until the thread ends, even of unreferenced WindowsHook variables. /// /// UI element functions may fail in hook procedures of low-level keyboard and mouse hooks. Workarounds exist. /// /// Exists an alternative way to monitor keyboard or mouse events - raw input API. Good: less overhead; can detect from which device the input event came. Bad: cannot block events; incompatible with low-level keyboard hooks. This library does not have functions to make the API easier to use. /// [DebuggerStepThrough] public sealed class WindowsHook : IDisposable { IntPtr _hh; //HHOOK readonly Api.HOOKPROC _proc1; //our intermediate dispatcher hook proc that calls _proc2 Delegate _proc2; //caller's hook proc readonly string _hookTypeString; //"Keyboard" etc readonly int _hookType; //Api.WH_ readonly bool _ignoreAuInjected; [ThreadStatic] static List t_antiGC; /// /// Sets a low-level keyboard hook (WH_KEYBOARD_LL). /// See API SetWindowsHookEx. /// /// New object that manages the hook. /// /// The hook procedure (function that handles hook events). /// Must return as soon as possible. More info: . /// If calls or (true), the event is not sent to apps and other hooks. /// Event data cannot be modified. /// NOTE: When the hook procedure returns, the parameter variable becomes invalid and unsafe to use. If you need the data for later use, copy its properties and not whole variable. /// /// Don't call the hook procedure for events sent by functions of this library. Default true. /// Set hook now. Default true. /// Failed. /// /// { /// print.it(x); /// if(x.vkCode == KKey.Escape) { stop = true; x.BlockEvent(); } /// }); /// dialog.show("hook"); /// //or /// //wait.doEventsUntil(-10, () => stop); //wait max 10 s for Esc key /// //print.it("the end"); /// ]]> /// public static WindowsHook Keyboard(Action hookProc, bool ignoreAuInjected = true, bool setNow = true) => new(Api.WH_KEYBOARD_LL, hookProc, setNow, 0, ignoreAuInjected); /// /// Sets a low-level mouse hook (WH_MOUSE_LL). /// See API SetWindowsHookEx. /// /// New object that manages the hook. /// /// The hook procedure (function that handles hook events). /// Must return as soon as possible. More info: . /// If calls or (true), the event is not sent to apps and other hooks. /// Event data cannot be modified. /// NOTE: When the hook procedure returns, the parameter variable becomes invalid and unsafe to use. If you need the data for later use, copy its properties and not whole variable. /// /// Don't call the hook procedure for events sent by functions of this library. Default true. /// Set hook now. Default true. /// Failed. /// /// { /// print.it(x); /// if(x.Event == HookData.MouseEvent.RightButton) { stop = x.IsButtonUp; x.BlockEvent(); } /// }); /// dialog.show("hook"); /// //or /// //wait.doEventsUntil(-10, () => stop); //wait max 10 s for right-click /// //print.it("the end"); /// ]]> /// public static WindowsHook Mouse(Action hookProc, bool ignoreAuInjected = true, bool setNow = true) => new(Api.WH_MOUSE_LL, hookProc, setNow, 0, ignoreAuInjected); internal static WindowsHook MouseRaw_(Func hookProc, bool ignoreAuInjected = true, bool setNow = true) => new(Api.WH_MOUSE_LL, hookProc, setNow, 0, ignoreAuInjected, "Mouse"); /// /// Sets a WH_CBT hook for a thread of this process. /// See API SetWindowsHookEx. /// /// New object that manages the hook. /// /// Hook procedure (function that handles hook events). /// Must return as soon as possible. /// If returns true, the event is canceled. For some events you can modify some fields of event data. /// NOTE: When the hook procedure returns, the parameter variable becomes invalid and unsafe to use. If you need the data for later use, copy its properties and not the variable. /// /// Native thread id, or 0 for this thread. The thread must belong to this process. /// Set hook now. Default true. /// Failed. /// /// { /// print.it(x.code); /// switch(x.code) { /// case HookData.CbtEvent.ACTIVATE: /// print.it(x.Hwnd); /// break; /// case HookData.CbtEvent.CREATEWND: /// var c=x.CreationInfo->lpcs; /// print.it(x.Hwnd, c->x, c->lpszName); /// c->x=500; /// break; /// } /// return false; /// }); /// dialog.showOkCancel("hook"); /// //new Form().ShowDialog(); //to test MINMAX /// ]]> /// public static WindowsHook ThreadCbt(Func hookProc, int threadId = 0, bool setNow = true) => new(Api.WH_CBT, hookProc, setNow, threadId); /// /// Sets a WH_GETMESSAGE hook for a thread of this process. /// See API SetWindowsHookEx. /// /// New object that manages the hook. /// /// The hook procedure (function that handles hook events). /// Must return as soon as possible. /// The event cannot be canceled. As a workaround, you can set msg->message=0. Also can modify other fields. /// NOTE: When the hook procedure returns, the pointer field of the parameter variable becomes invalid and unsafe to use. /// /// Native thread id, or 0 for this thread. The thread must belong to this process. /// Set hook now. Default true. /// Failed. /// /// { /// print.it(x.msg->ToString(), x.PM_NOREMOVE); /// }); /// dialog.show("hook"); /// ]]> /// public static WindowsHook ThreadGetMessage(Action hookProc, int threadId = 0, bool setNow = true) => new(Api.WH_GETMESSAGE, hookProc, setNow, threadId); /// /// Sets a WH_GETMESSAGE hook for a thread of this process. /// See API SetWindowsHookEx. /// /// New object that manages the hook. /// /// The hook procedure (function that handles hook events). /// Must return as soon as possible. /// If returns true, the event is canceled. /// /// Native thread id, or 0 for this thread. The thread must belong to this process. /// Set hook now. Default true. /// Failed. /// /// { /// print.it(x.key, 0 != (x.lParam & 0x80000000) ? "up" : "", x.lParam, x.PM_NOREMOVE); /// return false; /// }); /// dialog.show("hook"); /// ]]> /// public static WindowsHook ThreadKeyboard(Func hookProc, int threadId = 0, bool setNow = true) => new(Api.WH_KEYBOARD, hookProc, setNow, threadId); /// /// Sets a WH_MOUSE hook for a thread of this process. /// See API SetWindowsHookEx. /// /// New object that manages the hook. /// /// The hook procedure (function that handles hook events). /// Must return as soon as possible. /// If returns true, the event is canceled. /// NOTE: When the hook procedure returns, the pointer field of the parameter variable becomes invalid and unsafe to use. /// /// Native thread id, or 0 for this thread. The thread must belong to this process. /// Set hook now. Default true. /// Failed. /// /// { /// print.it(x.message, x.m->pt, x.m->hwnd, x.PM_NOREMOVE); /// return false; /// }); /// dialog.show("hook"); /// ]]> /// public static WindowsHook ThreadMouse(Func hookProc, int threadId = 0, bool setNow = true) => new(Api.WH_MOUSE, hookProc, setNow, threadId); /// /// Sets a WH_CALLWNDPROC hook for a thread of this process. /// See API SetWindowsHookEx. /// /// A new object that manages the hook. /// /// The hook procedure (function that handles hook events). /// Must return as soon as possible. /// The event cannot be canceled or modified. /// NOTE: When the hook procedure returns, the pointer field of the parameter variable becomes invalid and unsafe to use. /// /// Native thread id, or 0 for this thread. The thread must belong to this process. /// Set hook now. Default true. /// Failed. /// /// { /// ref var m = ref *x.msg; /// WndUtil.PrintMsg(out var s, m.hwnd, m.message, m.wParam, m.lParam); /// print.it(s, x.sentByOtherThread); /// }); /// dialog.show("hook"); /// ]]> /// public static WindowsHook ThreadCallWndProc(Action hookProc, int threadId = 0, bool setNow = true) => new(Api.WH_CALLWNDPROC, hookProc, setNow, threadId); /// /// Sets a WH_CALLWNDPROCRET hook for a thread of this process. /// See API SetWindowsHookEx. /// /// public static WindowsHook ThreadCallWndProcRet(Action hookProc, int threadId = 0, bool setNow = true) => new(Api.WH_CALLWNDPROCRET, hookProc, setNow, threadId); WindowsHook(int hookType, Delegate hookProc, bool setNow, int tid, bool ignoreAuInjected = false, [CallerMemberName] string m_ = null) { Not_.Null(hookProc); _proc2 = hookProc; _hookType = hookType; _hookTypeString = m_; _ignoreAuInjected = ignoreAuInjected; if (hookType is Api.WH_KEYBOARD_LL or Api.WH_MOUSE_LL) { _proc1 = _HookProcLL; //JIT-compile our hook proc and some functions it may call. OS gives us only 300 ms by default. if (!s_jit1) { s_jit1 = true; Jit_.Compile(typeof(WindowsHook), nameof(_HookProcLL)); _ = perf.ms; _ = keys.KeyTypes_.IsMod(KKey.Shift) && _DontBlockMod; } } else { _proc1 = _HookProc; } if (setNow) Hook(tid); (t_antiGC ??= new()).Add(this); } static bool s_jit1; /// /// Sets the hook. /// /// If the hook type is a thread hook - thread id, or 0 for current thread. Else not used and must be 0. /// Failed. /// The hook is already set. /// threadId not 0 and the hook type is not a thread hook. /// /// Usually don't need to call this function, because the WindowsHook static methods that return a new WindowsHook object by default call it. /// public void Hook(int threadId = 0) { if (_proc2 == null) throw new ObjectDisposedException(nameof(WindowsHook)); if (_hh != default) throw new InvalidOperationException("The hook is already set."); if (_hookType is Api.WH_KEYBOARD_LL or Api.WH_MOUSE_LL) { if (threadId != 0) throw new ArgumentException("threadId must be 0"); } else if (threadId == 0) { threadId = Api.GetCurrentThreadId(); } _hh = Api.SetWindowsHookEx(_hookType, _proc1, default, threadId); if (_hh == default) throw new AuException(0, "*set hook"); } /// /// Removes the hook. /// /// /// Does nothing if already removed or wasn't set. /// Later you can call to set hook again. /// Note: call instead if will not need to hook again. /// public void Unhook() { if (_hh != default) { _Restore_UnhookOld(); bool ok = Api.UnhookWindowsHookEx(_hh); if (!ok) print.warning($"WindowsHook.Unhook() failed ({_hookTypeString}). {lastError.message}"); _hh = default; } } /// /// Rehooks this low-level keyboard or mouse hook. /// /// /// Low level hooks may be occasionally disabled by the OS or other hooks. Workaround - call this function eg every 10 s in same thread. For example use . Don't call too frequently, eg every 1 s. /// This function unhooks current hook and sets new hook. Ensures that no events are missed or duplicate during it. /// /// The hook type isn't low-level keyboard or mouse. public void Restore() { if (_hookType is not (Api.WH_KEYBOARD_LL or Api.WH_MOUSE_LL)) throw new InvalidOperationException(); if (_proc2 == null) throw new ObjectDisposedException(nameof(WindowsHook)); if (_hookType is Api.WH_KEYBOARD_LL && DontRestoreKeyboardHooks_) return; //If we simply unhook/hook here, some events are missed. // Restoring usually takes 0.2 - 0.5 ms. And it seems the new hook starts working with a delay. // If restoring every 10 s, could miss maybe 1/10000 triggers. // Tested: when restoring every 15 ms, missed 3/50 triggers. // Solution: unhook the old hook after several ms. To avoid duplicate events, unhook it in the hook proc too. #if false if (_hh != default) Api.UnhookWindowsHookEx(_hh); _hh = Api.SetWindowsHookEx(_hookType, _proc1, default, 0); if (_hh == default) throw new AuException(0, "*set hook"); #else _Restore_UnhookOld(); var hh = Api.SetWindowsHookEx(_hookType, _proc1, default, 0); if (hh != default) { if (_hh != default) timer.after(10, _ => _Restore_UnhookOld()); _oldHook = _hh; _hh = hh; } else { Debug_.Print("failed"); } #endif } IntPtr _oldHook; /// /// Can be used to temporarily disable of all keyboard hooks in all processes. /// For example when a hotkey control is focused. /// /// /// Restore is disabled when the number of =true calls is greater than the number of =false calls. /// internal static unsafe bool DontRestoreKeyboardHooks_ { get => SharedMemory_.Ptr->winHook.dontRestoreKeyboardHooks > 0; set => Interlocked.Add(ref SharedMemory_.Ptr->winHook.dontRestoreKeyboardHooks, value ? 1 : -1); } void _Restore_UnhookOld() { if (_oldHook != default) { bool ok = Api.UnhookWindowsHookEx(_oldHook); _oldHook = default; Debug_.PrintIf(!ok, "failed to unhook old"); } } /// /// Returns true if the hook is set. /// public bool IsSet => _hh != default; ///// ///// Disable warning "Non-disposed WindowsHook variable". ///// //public bool NoWarningNondisposed { get; set; } /// /// Calls and disposes this object. /// public void Dispose() { Unhook(); _proc2 = null; t_antiGC.Remove(this); GC.SuppressFinalize(this); } /// /// Prints a warning if the variable is not disposed. Cannot dispose in finalizer. /// ~WindowsHook() { //unhooking in finalizer thread makes no sense. Must unhook in same thread, else fails. if (_hh != default) print.warning($"Non-disposed WindowsHook ({_hookTypeString}) variable."); //ok if unhooked but not disposed. If we are here, the thread ended and therefore don't need to remove this from t_antiGC. } unsafe nint _HookProc(int code, nint wParam, nint lParam) { if (code >= 0) { try { bool eat = false; switch (_proc2) { case Func p: eat = p(new HookData.ThreadCbt(this, code, wParam, lParam)); break; case Action p: p(new HookData.ThreadGetMessage(this, wParam, lParam)); break; case Func p: eat = p(new HookData.ThreadKeyboard(this, code, wParam, lParam)); break; case Func p: eat = p(new HookData.ThreadMouse(this, code, wParam, lParam)); break; case Action p: p(new HookData.ThreadCallWndProc(this, wParam, lParam)); break; case Action p: p(new HookData.ThreadCallWndProcRet(this, wParam, lParam)); break; } if (eat) return 1; } catch (Exception ex) { OnException_(ex); } } return Api.CallNextHookEx(default, code, wParam, lParam); } unsafe nint _HookProcLL(int code, nint wParam, nint lParam) { _Restore_UnhookOld(); if (code >= 0) { try { //using var p1 = perf.local(); bool eat = false; long t1 = 0; Action pm1; Func pm2; switch (_proc2) { case Action p: var kll = (Api.KBDLLHOOKSTRUCT*)lParam; var vk = (KKey)kll->vkCode; if (kll->IsInjected) { if (kll->IsInjectedByAu) { if (kll->vkCode == 0) goto gr; //used to enable activating windows if (!kll->IsUp) Triggers.AutotextTriggers.ResetEverywhere = true; if (_ignoreAuInjected) goto gr; } if (vk == KKey.MouseX2 && kll->dwExtraInfo == 1354291109) goto gr; //QM2 sync code } else { //When keys.Internal_.ReleaseModAndCapsLock sends Shift to turn off CapsLock, // hooks receive a non-injected LShift down, CapsLock down/up and injected LShift up. // Our triggers would recover, but cannot auto-repeat. Better don't call the hookproc. if ((vk == KKey.CapsLock || vk == KKey.LShift) && _ignoreAuInjected && _IgnoreLShiftCaps) goto gr; //Test how our triggers recover when a modifier down or up event is lost. Or when triggers started while a modifier is down. //if(keys.isScrollLock) { // //if(vk == KKey.LCtrl && !kll->IsUp) { print.it("lost Ctrl down"); goto gr; } // if(vk == KKey.LCtrl && kll->IsUp) { print.it("lost Ctrl up"); goto gr; } //} } //if (keys.KeyTypes_.IsMod(vk) && _DontBlockMod) goto gr; //old version, creates problems t1 = perf.ms; //p1.Next(); p(new HookData.Keyboard(this, lParam)); //info: wParam is message, but it is not useful, everything is in lParam if (eat = kll->BlockEvent) { kll->BlockEvent = false; if (keys.KeyTypes_.IsMod(vk) && _DontBlockMod && kll->IsUp) eat = false; } break; case Action p: pm1 = p; pm2 = null; gm1: var mll = (Api.MSLLHOOKSTRUCT*)lParam; switch ((int)wParam) { case Api.WM_LBUTTONDOWN: case Api.WM_RBUTTONDOWN: Triggers.AutotextTriggers.ResetEverywhere = true; break; } if (_ignoreAuInjected && mll->IsInjectedByAu) goto gr; //API bug workaround. In DPI-scaled windows on click mhsLL->pt is logical, although on move/wheel is physical. Must be always physical. //At first noticed only on Win10. But then noticed the same on Win7, although used to be correct. OK on Win8.1. Maybe depends on some other conditions, eg UAC IL, DPI, multimonitor. //Now it seems the bug is fixed on Win10. Found on SO: "Microsoft fixed it in 10.0.14393"; it is version 1607, August 2, 2016; but I cannot confirm it. //The wrong coords are the same as GetCursorPos. Only GetPhysicalCursorPos does not lie. Api.GetCursorPos is mapped to GetPhysicalCursorPos. //Note: on WM_MOUSEMOVE Get[Physical]CursorPos returns previous coords. On other messages same as hook. if (wParam != Api.WM_MOUSEMOVE /*&& osVersion.winVer < osVersion.win10*/) Api.GetCursorPos(out mll->pt); t1 = perf.ms; if (pm2 != null) { eat = pm2(wParam, lParam); } else { pm1(new HookData.Mouse(this, wParam, lParam)); if (eat = mll->BlockEvent) mll->BlockEvent = false; } break; case Func p: //raw mouse pm2 = p; pm1 = null; goto gm1; } //Prevent Windows disabling the low-level key/mouse hook. // Hook proc must return in HKEY_CURRENT_USER\Control Panel\Desktop:LowLevelHooksTimeout ms. // Default 300. On Win10 max 1000 (bigger registry value is ignored and used 1000). // On timeout Windows: // 1. Does not wait more. Passes the message to the next hook etc, and we cannot return 1 to block it. // 2. Kills the hook after several such cases. Usually 6 keys or 11 mouse events. // 3. Makes the hook useless: next times does not wait for it, and we cannot return 1 to block the event. // Somehow does not apply 2 and 3 to some apps, eg C# apps created by Visual Studio, although applies to those created not by VS. I did not find why. if (t1 != 0 && (t1 = perf.ms - t1) > 200 && !Debugger.IsAttached) { if (t1 > LowLevelHooksTimeout - 50) { var s1 = _hookType == Api.WH_KEYBOARD_LL ? "key" : "mouse"; var s2 = eat ? $" On timeout the {s1} message is passed to the active window, other hooks, etc." : null; //print.warning($"Possible hook timeout. Hook procedure time: {t1} ms. LowLevelHooksTimeout: {LowLevelHooksTimeout} ms.{s2}"); //too slow first time //print.it($"Warning: Possible hook timeout. Hook procedure time: {t1} ms. LowLevelHooksTimeout: {LowLevelHooksTimeout} ms.{s2}\r\n{new StackTrace(0, false)}"); //first Write() JIT 30 ms ThreadPool.QueueUserWorkItem(s3 => print.it(s3), $"Warning: Possible hook timeout. Hook procedure time: {t1} ms. LowLevelHooksTimeout: {LowLevelHooksTimeout} ms.{s2}\r\n{new StackTrace(0, false)}"); //fast if with false. But async print can be confusing. } //FUTURE: print warning if t1 is >25 frequently. Unhook and don't rehook if >LowLevelHooksTimeout frequently. Unhook(); _hh = Api.SetWindowsHookEx(_hookType, _proc1, default, 0); } if (eat) return 1; } catch (Exception ex) { OnException_(ex); } } gr: return Api.CallNextHookEx(default, code, wParam, lParam); } /// /// Gets the max time in milliseconds allowed by Windows for low-level keyboard and mouse hook procedures. /// /// /// Gets registry value HKEY_CURRENT_USER\Control Panel\Desktop:LowLevelHooksTimeout. If it is missing, returns 300; it is the default value used by Windows. If greater than 1000, returns 1000, because Windows 10 ignores bigger values. /// /// If a hook procedure takes more time, Windows does not wait. Then its return value is ignored, and the event is passed to other apps, hooks, etc. After several such cases Windows may fully or partially disable the hook. This class detects such cases; then restores the hook and prints a warning. If the warning is rare, you can ignore it. If frequent, it means your hook procedure is too slow. /// /// Callback functions of keyboard and mouse triggers are called in a hook procedure, therefore must be as fast as possible. More info: . /// /// More info: registry LowLevelHooksTimeout. /// /// Note: After changing the timeout in registry, it is not applied immediately. Need to log off/on. /// public static int LowLevelHooksTimeout { get { if (s_lowLevelHooksTimeout == 0) { //default 300, tested on Win10 and 7 //max 1000 on Win10. On Win7 more. Not tested on Win8. On Win7/8 may be changed by a Windows update. s_lowLevelHooksTimeout = Microsoft.Win32.Registry.GetValue(@"HKEY_CURRENT_USER\Control Panel\Desktop", "LowLevelHooksTimeout", null) is int v ? (int)Math.Min(1000u, (uint)v) : 300; } return s_lowLevelHooksTimeout; } internal set { int v = Math.Clamp(value, 0, 5000); Microsoft.Win32.Registry.SetValue(@"HKEY_CURRENT_USER\Control Panel\Desktop", "LowLevelHooksTimeout", v); s_lowLevelHooksTimeout = v; } } static int s_lowLevelHooksTimeout; internal static void OnException_(Exception e) { print.warning("Unhandled exception in hook procedure. " + e.ToString(), -1); } [StructLayout(LayoutKind.Sequential, Size = 32)] //note: this struct is in shared memory. Size must be same in all library versions. internal struct SharedMemoryData_ { public long dontBlockModUntil, dontBlocLShiftCapsUntil; public int dontRestoreKeyboardHooks; //12 bytes reserved } /// /// Let other hooks (in all processes) don't block modifier key up events for timeMS milliseconds. If 0 - restore. /// Used by mouse triggers waiting for mod keys released, to prevent inputblockers blocking mod up events, eg when sending keys/text. /// Returns the timeout time (Environment.TickCount64 + timeMS) or 0. /// internal unsafe long DontBlockModInOtherHooks_(long timeMS) { _ignoreModExceptThisHook = timeMS > 0; var r = _ignoreModExceptThisHook ? Environment.TickCount64 + timeMS : 0; SharedMemory_.Ptr->winHook.dontBlockModUntil = r; return r; } unsafe bool _DontBlockMod => SharedMemory_.Ptr->winHook.dontBlockModUntil > Environment.TickCount64 && !_ignoreModExceptThisHook; bool _ignoreModExceptThisHook; /// /// Let all hooks (in all processes) ignore LShift and CapsLock for timeMS milliseconds. If 0 - restore. /// Returns the timeout time (Environment.TickCount64 + timeMS) or 0. /// Used when turning off CapsLock with Shift. /// internal static unsafe long IgnoreLShiftCaps_(long timeMS) { var r = timeMS > 0 ? Environment.TickCount64 + timeMS : 0; SharedMemory_.Ptr->winHook.dontBlocLShiftCapsUntil = r; return r; } static unsafe bool _IgnoreLShiftCaps => SharedMemory_.Ptr->winHook.dontBlocLShiftCapsUntil > Environment.TickCount64; } } namespace Au.Types { /// /// Contains types of hook data for hook procedures set by and . /// public static partial class HookData { /// /// Event data for the hook procedure set by . /// More info: API LowLevelKeyboardProc. /// public unsafe struct Keyboard { /// The caller object of your hook procedure. For example can be used to unhook. public readonly WindowsHook hook; readonly Api.KBDLLHOOKSTRUCT* _x; internal Keyboard(WindowsHook hook, nint lParam) { this.hook = hook; _x = (Api.KBDLLHOOKSTRUCT*)lParam; } /// /// Call this function to steal this event from other hooks and apps. /// public void BlockEvent() => _x->BlockEvent = true; /// /// Is extended key. /// public bool IsExtended => 0 != (_x->flags & Api.LLKHF_EXTENDED); /// /// true if the event was generated by API such as SendInput. /// false if the event was generated by the keyboard. /// public bool IsInjected => 0 != (_x->flags & Api.LLKHF_INJECTED); /// /// true if the event was generated by functions of this library. /// public bool IsInjectedByAu => 0 != (_x->flags & Api.LLKHF_INJECTED) && _x->dwExtraInfo == Api.AuExtraInfo; /// /// Key Alt is pressed. /// public bool IsAlt => 0 != (_x->flags & Api.LLKHF_ALTDOWN); /// /// Is key-up event. /// public bool IsUp => 0 != (_x->flags & Api.LLKHF_UP); /// /// If the key is a modifier key (Shift, Ctrl, Alt, Win), returns the modifier flag. Else returns 0. /// public KMod Mod => keys.Internal_.KeyToMod((KKey)_x->vkCode); /// /// If is a left or right modifier key code (LShift, LCtrl, LAlt, RShift, RCtrl, RAlt, RWin), returns the common modifier key code (Shift, Ctrl, Alt, Win). Else returns . /// public KKey Key { get { var vk = (KKey)_x->vkCode; switch (vk) { case KKey.LShift: case KKey.RShift: return KKey.Shift; case KKey.LCtrl: case KKey.RCtrl: return KKey.Ctrl; case KKey.LAlt: case KKey.RAlt: return KKey.Alt; case KKey.RWin: return KKey.Win; } return vk; } } /// /// Returns true if key == or key is Shift, Ctrl, Alt or Win and is LShift/RShift, LCtrl/RCtrl, LAlt/RAlt or RWin. /// public bool IsKey(KKey key) { var vk = (KKey)_x->vkCode; if (key == vk) return true; switch (key) { case KKey.Shift: return vk == KKey.LShift || vk == KKey.RShift; case KKey.Ctrl: return vk == KKey.LCtrl || vk == KKey.RCtrl; case KKey.Alt: return vk == KKey.LAlt || vk == KKey.RAlt; case KKey.Win: return vk == KKey.RWin; } return false; } /// /// Converts flags to API SendInput flags KEYEVENTF_KEYUP and KEYEVENTF_EXTENDEDKEY. /// internal byte SendInputFlags_ { get { uint f = 0; if (IsUp) f |= Api.KEYEVENTF_KEYUP; if (IsExtended) f |= Api.KEYEVENTF_EXTENDEDKEY; return (byte)f; } } /// public override string ToString() { return $"{vkCode.ToString()} {(IsUp ? "up" : "")}{(IsInjected ? " (injected)" : "")}"; } /// API KBDLLHOOKSTRUCT public KKey vkCode => (KKey)_x->vkCode; /// API KBDLLHOOKSTRUCT public uint scanCode => _x->scanCode; /// API KBDLLHOOKSTRUCT public uint flags => _x->flags; /// API KBDLLHOOKSTRUCT public int time => _x->time; /// API KBDLLHOOKSTRUCT public nint dwExtraInfo => _x->dwExtraInfo; internal Api.KBDLLHOOKSTRUCT* NativeStructPtr_ => _x; } /// /// Extra info value used by functions of this library that generate keyboard events. Low-level hooks receive it in dwExtraInfo. /// public const int AuExtraInfo = Api.AuExtraInfo; /// /// Hook data for the hook procedure set by . /// More info: API LowLevelMouseProc. /// public unsafe struct Mouse { /// The caller object of your hook procedure. For example can be used to unhook. public readonly WindowsHook hook; readonly Api.MSLLHOOKSTRUCT* _x; readonly MouseEvent _event; internal Mouse(WindowsHook hook, nint wParam, nint lParam) { this.hook = hook; var p = (Api.MSLLHOOKSTRUCT*)lParam; _x = p; int e = (int)wParam; switch (e) { case Api.WM_MOUSEMOVE: IsMove = true; break; case Api.WM_LBUTTONDOWN: case Api.WM_RBUTTONDOWN: case Api.WM_MBUTTONDOWN: IsButtonDown = true; break; case Api.WM_LBUTTONUP: case Api.WM_RBUTTONUP: case Api.WM_MBUTTONUP: e--; IsButtonUp = true; break; case Api.WM_XBUTTONUP: e--; IsButtonUp = true; goto g1; case Api.WM_XBUTTONDOWN: IsButtonDown = true; g1: switch (p->mouseData >> 16) { case 1: e |= 0x1000; break; case 2: e |= 0x2000; break; } break; case Api.WM_MOUSEWHEEL: case Api.WM_MOUSEHWHEEL: IsWheel = true; int wheel = (short)(p->mouseData >> 16); if (wheel > 0) e |= 0x1000; else if (wheel < 0) e |= 0x2000; WheelValue = wheel; break; } _event = (MouseEvent)e; } /// /// Call this function to steal this event from other hooks and apps. /// public void BlockEvent() => _x->BlockEvent = true; /// /// What event it is (button, move, wheel). /// public MouseEvent Event => _event; /// /// Is mouse-move event. /// public bool IsMove { get; } /// /// Is button-down event. /// public bool IsButtonDown { get; } /// /// Is button-up event. /// public bool IsButtonUp { get; } /// /// Is button event (down or up). /// public bool IsButton => IsButtonDown | IsButtonUp; /// /// Converts to . /// /// Left, Right, Middle, X1, X2 or 0. The down/up/double flags not used. public MButton Button { get { return _event switch { MouseEvent.LeftButton => MButton.Left, MouseEvent.RightButton => MButton.Right, MouseEvent.MiddleButton => MButton.Middle, MouseEvent.X1Button => MButton.X1, MouseEvent.X2Button => MButton.X2, _ => 0, }; } } /// /// Is wheel event. /// public bool IsWheel { get; } /// /// true if the event was generated by API such as SendInput. /// false if the event was generated by the mouse. /// public bool IsInjected => 0 != (flags & Api.LLMHF_INJECTED); /// /// true if the event was generated by functions of this library. /// public bool IsInjectedByAu => IsInjected && dwExtraInfo == Api.AuExtraInfo; /// /// Wheel rotation amount, 120 for 1 full tick. Negative if backward. /// Usually 120 or -120, but some devices or software may produce smaller or bigger values. /// public int WheelValue { get; } /// public override string ToString() { var ud = ""; if (IsButtonDown) ud = "down"; else if (IsButtonUp) ud = "up"; return $"{Event.ToString()} {ud} {pt.ToString()}{(IsInjected ? " (injected)" : "")}"; } /// API MSLLHOOKSTRUCT public POINT pt => _x->pt; /// API MSLLHOOKSTRUCT public uint mouseData => _x->mouseData; /// API MSLLHOOKSTRUCT public uint flags => _x->flags; /// API MSLLHOOKSTRUCT public int time => _x->time; /// API MSLLHOOKSTRUCT public nint dwExtraInfo => _x->dwExtraInfo; internal Api.MSLLHOOKSTRUCT* NativeStructPtr_ => _x; } /// /// Mouse hook event types. See . /// public enum MouseEvent { #pragma warning disable 1591 //no XML doc Move = 0x0200, //WM_MOUSEMOVE LeftButton = 0x0201, //WM_LBUTTONDOWN RightButton = 0x0204, //WM_RBUTTONDOWN MiddleButton = 0x0207, //WM_MBUTTONDOWN X1Button = 0x120B, //WM_XBUTTONDOWN | 0x1000 X2Button = 0x220B, //WM_XBUTTONDOWN | 0x2000 WheelForward = 0x120A, //WM_WHEEL | 0x1000 WheelBackward = 0x220A, //WM_WHEEL | 0x2000 WheelRight = 0x120E, //WM_HWHEEL | 0x1000 WheelLeft = 0x220E, //WM_HWHEEL | 0x2000 #pragma warning restore 1591 } /// /// Hook data for the hook procedure set by . /// More info: API CBTProc. /// public struct ThreadCbt { /// The caller object of your hook procedure. For example can be used to unhook. public readonly WindowsHook hook; /// API CBTProc public readonly CbtEvent code; /// API CBTProc public readonly nint wParam; /// API CBTProc public readonly nint lParam; /// Window handle. public wnd Hwnd => code switch { CbtEvent.ACTIVATE or CbtEvent.CREATEWND or CbtEvent.DESTROYWND or CbtEvent.MINMAX or CbtEvent.MOVESIZE or CbtEvent.SETFOCUS => (wnd)wParam, _ => default }; internal ThreadCbt(WindowsHook hook, int code, nint wParam, nint lParam) { this.hook = hook; this.code = (CbtEvent)code; this.wParam = wParam; this.lParam = lParam; } /// /// Gets event info. /// /// is not . public unsafe CBTACTIVATESTRUCT* ActivationInfo => code == CbtEvent.ACTIVATE ? (CBTACTIVATESTRUCT*)lParam : throw new InvalidOperationException(); /// /// API CBTACTIVATESTRUCT. /// public struct CBTACTIVATESTRUCT { /// public bool fMouse; /// public wnd hWndActive; } /// /// Gets event info. /// You can modify x, y, cx, cy, and hwndInsertAfter. /// /// is not . public unsafe CBT_CREATEWND* CreationInfo => code == CbtEvent.CREATEWND ? (CBT_CREATEWND*)lParam : throw new InvalidOperationException(); /// /// API CBT_CREATEWND. /// public unsafe struct CBT_CREATEWND { /// public CREATESTRUCT* lpcs; /// public wnd hwndInsertAfter; } //rejected. Rarely used or too simple. ///// ///// Gets event info. Returns the mouse message. ///// ///// MOUSEHOOKSTRUCT. ///// is not CbtEvent.CLICKSKIPPED. //public unsafe uint MouseInfo(out MOUSEHOOKSTRUCT* m) { // if (code != CbtEvent.CLICKSKIPPED) throw new InvalidOperationException(); // m = (MOUSEHOOKSTRUCT*)lParam; // return (uint)wParam; //} ///// ///// Gets event info. Returns the key code. ///// ///// lParam of the key message. Specifies the repeat count, scan code, etc. See API WM_KEYDOWN. ///// is not CbtEvent.KEYSKIPPED. //public KKey KeyInfo(out uint lParam) { // if (code != CbtEvent.KEYSKIPPED) throw new InvalidOperationException(); // lParam = (uint)this.lParam; // return (KKey)(uint)wParam; //} ///// ///// Gets event info. Returns the window handle. ///// ///// The previously focused window, or default(wnd). ///// is not CbtEvent.SETFOCUS. //public wnd FocusInfo(out wnd wLostFocus) { // if (code != CbtEvent.SETFOCUS) throw new InvalidOperationException(); // wLostFocus = (wnd)lParam; // return (wnd)wParam; //} ///// ///// Gets event info. ///// ///// is not CbtEvent.MOVESIZE. //public unsafe RECT* MoveSizeInfo => code == CbtEvent.MOVESIZE ? (RECT*)lParam : throw new InvalidOperationException(); ///// ///// Gets event info. ///// Returns the new show state. See API ShowWindow. Minimized 6, maximized 3, restored 9. ///// ///// is not CbtEvent.MINMAX. //public int MinMaxInfo => code == CbtEvent.MINMAX ? (int)lParam & 0xffff : throw new InvalidOperationException(); } /// /// CBT hook event types. Used with . /// More info: API CBTProc. /// public enum CbtEvent { #pragma warning disable 1591 //no XML doc MOVESIZE = 0, MINMAX = 1, //QS = 2, CREATEWND = 3, DESTROYWND = 4, ACTIVATE = 5, CLICKSKIPPED = 6, KEYSKIPPED = 7, SYSCOMMAND = 8, SETFOCUS = 9, #pragma warning restore 1591 } /// /// Hook data for the hook procedure set by . /// More info: API GetMsgProc. /// public unsafe struct ThreadGetMessage { /// The caller object of your hook procedure. For example can be used to unhook. public readonly WindowsHook hook; /// /// The message has not been removed from the queue, because called API PeekMessage with flag PM_NOREMOVE. /// public readonly bool PM_NOREMOVE; /// /// Message parameters. /// API MSG. /// public readonly MSG* msg; internal ThreadGetMessage(WindowsHook hook, nint wParam, nint lParam) { this.hook = hook; PM_NOREMOVE = (uint)wParam == Api.PM_NOREMOVE; msg = (MSG*)lParam; } } /// /// Hook data for the hook procedure set by . /// More info: API KeyboardProc. /// public struct ThreadKeyboard { /// The caller object of your hook procedure. For example can be used to unhook. public readonly WindowsHook hook; /// /// The message has not been removed from the queue, because called API PeekMessage with flag PM_NOREMOVE. /// public readonly bool PM_NOREMOVE; /// /// The key code. /// public readonly KKey key; /// /// lParam of the key message. Specifies the key state, scan code, etc. See API KeyboardProc. /// public readonly uint lParam; /// /// Is key-up event. /// public bool IsUp => 0 != (lParam & 0x80000000); internal ThreadKeyboard(WindowsHook hook, int code, nint wParam, nint lParam) { this.hook = hook; PM_NOREMOVE = code == Api.HC_NOREMOVE; key = (KKey)(uint)wParam; this.lParam = (uint)lParam; } } /// /// Hook data for the hook procedure set by . /// More info: API MouseProc. /// public unsafe struct ThreadMouse { /// The caller object of your hook procedure. For example can be used to unhook. public readonly WindowsHook hook; /// /// The message has not been removed from the queue, because called API PeekMessage with flag PM_NOREMOVE. /// public readonly bool PM_NOREMOVE; /// /// The mouse message, for example WM_MOUSEMOVE. /// public readonly uint message; /// /// More info about the mouse message. /// API MOUSEHOOKSTRUCT. /// public readonly MOUSEHOOKSTRUCT* m; internal ThreadMouse(WindowsHook hook, int code, nint wParam, nint lParam) { this.hook = hook; PM_NOREMOVE = code == Api.HC_NOREMOVE; message = (uint)wParam; m = (MOUSEHOOKSTRUCT*)lParam; } } #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member /// API MOUSEHOOKSTRUCT public struct MOUSEHOOKSTRUCT { public POINT pt; public wnd hwnd; public int wHitTestCode; public nint dwExtraInfo; } /// API CWPSTRUCT public struct CWPSTRUCT { public nint lParam; public nint wParam; public int message; public wnd hwnd; } /// API CWPRETSTRUCT public struct CWPRETSTRUCT { public nint lResult; public nint lParam; public nint wParam; public int message; public wnd hwnd; } #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member /// /// Hook data for the hook procedure set by . /// More info: API CallWndProc. /// public unsafe struct ThreadCallWndProc { /// The caller object of your hook procedure. For example can be used to unhook. public readonly WindowsHook hook; /// /// True if the message was sent by another thread. /// public readonly bool sentByOtherThread; //note: incorrect info in MSDN /// /// Message parameters. /// API CWPSTRUCT. /// public readonly CWPSTRUCT* msg; internal ThreadCallWndProc(WindowsHook hook, nint wParam, nint lParam) { this.hook = hook; sentByOtherThread = 0 != wParam; msg = (CWPSTRUCT*)lParam; } } /// /// Hook data for the hook procedure set by . /// More info: API CallWndRetProc. /// public unsafe struct ThreadCallWndProcRet { /// The caller object of your hook procedure. For example can be used to unhook. public readonly WindowsHook hook; /// /// True if the message was sent by another thread. /// public readonly bool sentByOtherThread; //note: incorrect info in MSDN /// /// Message parameters and the return value. /// API CWPRETSTRUCT. /// public readonly CWPRETSTRUCT* msg; internal ThreadCallWndProcRet(WindowsHook hook, nint wParam, nint lParam) { this.hook = hook; sentByOtherThread = 0 != wParam; msg = (CWPRETSTRUCT*)lParam; } } /// /// Calls API API ReplyMessage, which allows to use and COM in the hook procedure. /// /// /// Don't notify the target window about the event, and don't call other hook procedures. /// This value is used instead of the return value of the hook procedure, which is ignored. /// /// /// It can be used as a workaround for this problem: in low-level hook procedure some functions don't work with some windows. For example cannot get a UI element or use a COM object. Error/exception "An outgoing call cannot be made since the application is dispatching an input-synchronous call (0x8001010D)". /// public static void ReplyMessage(bool cancelEvent) => Api.ReplyMessage(cancelEvent ? 1 : 0); } } ================================================ FILE: Au/Au.More/WinformsControlNames.cs ================================================ namespace Au.More; /// /// Gets programming names of .NET Windows Forms controls. /// /// /// Usually each control has a unique name. It's the property. Useful to identify controls without a classic name/text. /// The control id of these controls is not useful, it is not constant. /// public sealed class WinformsControlNames : IDisposable { ProcessMemory _pm; wnd _w; /// public void Dispose() { if (_pm != null) { _pm.Dispose(); _pm = null; } GC.SuppressFinalize(this); } static readonly int WM_GETCONTROLNAME = Api.RegisterWindowMessage("WM_GETCONTROLNAME"); /// /// Prepares to get control names. /// /// Any top-level or child window of that process. /// w invalid. /// Failed to allocate process memory (see ) needed to get control names, usually because of [](xref:uac). public WinformsControlNames(wnd w) { _pm = new ProcessMemory(w, 4096); //throws _w = w; } /// /// Gets control name. /// /// null if failed or the name is empty. /// The control. Can be a top-level window too. Must be of the same process as the window specified in the constructor. public string GetControlName(wnd c) { if (_pm == null) return null; if (!IsWinformsControl(c)) return null; if (!c.SendTimeout(5000, out var R, WM_GETCONTROLNAME, 4096, _pm.Mem) || (int)R < 1) return null; int len = (int)R - 1; if (len == 0) return ""; return _pm.ReadCharString(len); } /// /// Returns true if window class name starts with "WindowsForms". /// Usually it means that we can get Windows Forms control name of w and its child controls. /// /// The window. Can be top-level or control. public static bool IsWinformsControl(wnd w) { return w.ClassNameIs("WindowsForms*"); } /// /// Gets the programming name of a Windows Forms control. /// /// null if it is not a Windows Forms control or if failed. /// The control. Can be top-level window too. /// /// This function is easy to use and does not throw exceptions. However, when you need names of multiple controls of a single window, better create a instance (once) and for each control call its GetControlName method, it will be faster. public static string GetSingleControlName(wnd c) { if (!IsWinformsControl(c)) return null; try { using (var x = new WinformsControlNames(c)) return x.GetControlName(c); } catch { } return null; } //Don't use this cached version, it does not make significantly faster. Also, keeping process handle in such a way is not good, would need to use other thread to close it after some time. ///// ///// Gets programming name of a Windows Forms control. ///// Returns null if it is not a Windows Forms control or if failed. ///// ///// The control. Can be top-level window too. ///// When need to get control names repeatedly or quite often, this function can be faster than creating instance each time and calling its GetControlName method, because this function remembers the last used process etc. Also it is easier to use and does not throw exceptions. //public static string GetSingleControlName(wnd c) //{ // if(!IsWinformsControl(c)) return null; // uint pid = c.ProcessId; if(pid == 0) return null; // lock (_prevLock) { // if(pid != _prevPID || perf.ms - _prevTime > 1000) { // if(_prev != null) { _prev.Dispose(); _prev = null; } // try { _prev = new WinformsControlNames(c); } catch { } // //print.it("new"); // } //else print.it("cached"); // _prevPID = pid; _prevTime = perf.ms; // if(_prev == null) return null; // return _prev.GetControlName(c); // } //} //static WinformsControlNames _prev; static uint _prevPID; static long _prevTime; static object _prevLock = new object(); //cache } ================================================ FILE: Au/Au.Types/ColorInt.cs ================================================ using System.Drawing; using System.Text.Json.Serialization; namespace Au.Types; //rejected: /// /// /// Color, as int in 0xAARRGGBB format. /// Can convert from/to , , int (0xAARRGGBB), Windows COLORREF (0xBBGGRR), string. /// public record struct ColorInt { /// /// Color value in 0xAARRGGBB format. /// [JsonInclude] public int argb; /// Color value in 0xAARRGGBB or 0xRRGGBB format. /// Set alpha = 0xFF. If null (default), sets alpha = 0xFF if it is 0 in colorBGR. public ColorInt(int colorARGB, bool? makeOpaque = null) { if (makeOpaque == true || (makeOpaque == null && (colorARGB & ~0xFFFFFF) == 0)) colorARGB |= 0xFF << 24; argb = colorARGB; } /// Color value in 0xAARRGGBB or 0xRRGGBB format. /// Set alpha = 0xFF. If null (default), sets alpha = 0xFF if it is 0 in colorBGR. public ColorInt(uint colorARGB, bool? makeOpaque) : this((int)colorARGB, makeOpaque) { } /// /// Converts from an int color value in 0xRRGGBB or 0xAARRGGBB format. /// Sets alpha = 0xFF if it is 0 in color. /// //[Obsolete] //to find all references public static implicit operator ColorInt(int color) => new(color); /// /// Converts from an uint color value in 0xRRGGBB or 0xAARRGGBB format. /// Sets alpha = 0xFF if it is 0 in color. /// //[Obsolete] //to find all references public static implicit operator ColorInt(uint color) => new((int)color); /// /// Converts from . /// public static implicit operator ColorInt(Color color) => new(color.ToArgb(), false); /// /// Converts from . /// public static implicit operator ColorInt(System.Windows.Media.Color color) => new((color.A << 24) | (color.R << 16) | (color.G << 8) | color.B, false); /// /// Converts from a color name () or string "0xRRGGBB" or "#RRGGBB". /// /// /// If s is a hex number that contains 6 or less hex digits, makes opaque (alpha 0xFF). /// If s is null or invalid, sets c.argb = 0 and returns false. /// public static bool FromString(string s, out ColorInt c) { c.argb = 0; if (s == null || s.Length < 2) return false; if (s[0] == '0' && s[1] == 'x') { c.argb = s.ToInt(0, out int end); if (end < 3) return false; if (end <= 8) c.argb |= unchecked((int)0xFF000000); } else if (s[0] == '#') { c.argb = s.ToInt(1, out int end, STIFlags.IsHexWithout0x); if (end < 2) return false; if (end <= 7) c.argb |= unchecked((int)0xFF000000); } else { c.argb = Color.FromName(s).ToArgb(); if (c.argb == 0) return false; //invalid is 0, black is 0xFF000000 } return true; } /// /// Converts from Windows native COLORREF (0xBBGGRR to 0xAARRGGBB). /// /// Color in 0xBBGGRR format. /// Set alpha = 0xFF. If null (default), sets alpha = 0xFF if it is 0 in colorBGR. public static ColorInt FromBGR(int colorBGR, bool? makeOpaque = null) => new(SwapRB(colorBGR), makeOpaque); /// /// Converts to Windows native COLORREF (0xBBGGRR from 0xAARRGGBB). /// /// color in COLORREF format. Does not modify this variable. /// Set the alpha byte = 0. public int ToBGR(bool zeroAlpha = true) { var r = SwapRB(argb); if (zeroAlpha) r &= 0xFFFFFF; return r; } //rejected. Easy to create bugs when actually need BGR. Let use ToBGR() when need BGR, or argb field when need ARGB. ///// Returns c.argb. //public static explicit operator int(ColorInt c) => c.argb; ///// Returns (uint)c.argb. //public static explicit operator uint(ColorInt c) => (uint)c.argb; /// Converts to . public static explicit operator Color(ColorInt c) => Color.FromArgb(c.argb); /// Converts to . public static explicit operator System.Windows.Media.Color(ColorInt c) { uint k = (uint)c.argb; return System.Windows.Media.Color.FromArgb((byte)(k >> 24), (byte)(k >> 16), (byte)(k >> 8), (byte)k); } internal static System.Windows.Media.Color WpfColor_(int rgb) => System.Windows.Media.Color.FromRgb((byte)(rgb >> 16), (byte)(rgb >> 8), (byte)rgb); internal static System.Windows.Media.SolidColorBrush WpfBrush_(int rgb) => new(WpfColor_(rgb)); ///// ///// FromBGR(GetSysColor). ///// //internal static ColorInt FromSysColor_(int colorIndex) => FromBGR(Api.GetSysColor(colorIndex), true); /// public override string ToString() => "#" + argb.ToString("X8"); /// /// Converts color from ARGB (0xAARRGGBB) to ABGR (0xAABBGGRR) or vice versa (swaps the red and blue bytes). /// ARGB is used in .NET, GDI+ and HTML/CSS. /// ABGR is used by most Windows API; aka COLORREF. /// public static int SwapRB(int color) => (color & unchecked((int)0xff00ff00)) | (color << 16 & 0xff0000) | (color >> 16 & 0xff); /// public static uint SwapRB(uint color) => (color & 0xff00ff00) | (color << 16 & 0xff0000) | (color >> 16 & 0xff); //rejected. Unclear usage. Instead let users call ToHLS, change L how they want, and call FromHLS. ///// ///// Changes color's luminance (makes darker or brighter). ///// Returns new color. Does not modify this variable. ///// ///// The luminance in units of 0.1 percent of the range (which depends on totalRange). Can be from -1000 to 1000. ///// If true, n is in whole luminance range (from minimal to maximal possible). If false, n is in the range from current luminance of the color to the maximal (if n positive) or minimal (if n negative) luminance. ///// ///// Calls API ColorAdjustLuma. ///// Does not change hue and saturation. Does not use alpha. ///// //internal ColorInt AdjustLuminance(int n, bool totalRange = false) { // uint u = (uint)argb; // u = Api.ColorAdjustLuma(u & 0xffffff, n, !totalRange) | (u & 0xFF000000); // return new((int)u, false); // //tested: with SwapRB the same. //} /// /// Converts from hue-luminance-saturation (HLS). /// /// Hue, 0 to 240. /// Luminance, 0 to 240. /// Saturation, 0 to 240. /// Return color in 0xBBGGRR format. If false, 0xRRGGBB. /// Color in 0xRRGGBB or 0xBBGGRR format, depending on bgr. Alpha 0. public static int FromHLS(int H, int L, int S, bool bgr) { if (S == 0) { //ColorHLSToRGB bug: returns 0 if S 0 int i = L * 255 / 240; return i | (i << 8) | (i << 16); } int color = Api.ColorHLSToRGB((ushort)H, (ushort)L, (ushort)S); if (!bgr) color = SwapRB(color); return color; } /// /// Converts to hue-luminance-saturation (HLS). /// /// Color in 0xRRGGBB or 0xBBGGRR format, depending on bgr. Ignores alpha. /// color is in 0xBBGGRR format. If false, 0xRRGGBB. /// Hue, luminance and saturation. All 0 to 240. public static (int H, int L, int S) ToHLS(int color, bool bgr) { if (!bgr) color = SwapRB(color); Api.ColorRGBToHLS(color, out var H, out var L, out var S); return (H, L, S); } /// /// Calculates color's perceived brightness. /// /// 0 to 1. /// Color in 0xRRGGBB or 0xBBGGRR format, depending on bgr. Ignores alpha. /// color is in 0xBBGGRR format. If false, 0xRRGGBB. /// /// Unlike and , this function uses different weights for red, green and blue components. /// Ignores alpha. /// public static float GetPerceivedBrightness(int color, bool bgr) { uint u = (uint)color; if (bgr) u = SwapRB(u); uint R = u >> 16 & 0xff, G = u >> 8 & 0xff, B = u & 0xff; return (float)(Math.Sqrt(R * R * .299 + G * G * .587 + B * B * .114) / 255); } //same result as ColorAdjustLuma. Probably slower. //internal static int SetLuminance(int color, bool bgr, double percent, bool totalRange) { // var (H, L, S) = ToHLS(color, bgr); // L = (int)Math2.PercentToValue(totalRange ? 240 : L, percent); // return (int)((uint)FromHLS(H, Math.Clamp(L, 0, 240), S, bgr) | (color & 0xFF000000)); //} } ================================================ FILE: Au/Au.Types/JSettings.cs ================================================ using System.Text.Json; using System.Text.Json.Serialization; namespace Au.Types; /// /// Base of record classes that contain various settings as public fields or properties. Loads and lazily auto-saves from/to a JSON file. /// /// /// All functions are thread-safe. /// /// /// Load(File); /// /// // examples of settings /// public int i; /// public string s = "default"; /// public string[] a = []; /// public Dictionary d = new(); /// } /// ]]> /// public abstract record class JSettings : IDisposable { string _file; bool _loadedFile; byte[] _old; JsonSerializerOptions _jsOpt; readonly object _lock = new(); static readonly List s_list = new(); static int s_loadedOnce; /// /// Loads a JSON file and deserializes to an object of type T, or creates a new object of type T. /// /// Object of type T. Either deserialized from file or new object with default values. /// Full path of .json file. If null, does not load and will not save. /// Use default settings. Don't load from file. Delete file if exists. /// What to do if failed to load or parse the file: true (default) - backup (rename) the file and use default settings; false - throw exception (for example if invalid JSON); null - show dialog with options to exit or use default settings. /// Options for deserializing and serializing. If null, uses . /// Not full path. /// Field type not supported by . protected static T Load(string file, bool useDefault = false, bool? useDefaultOnError = true, JsonSerializerOptions jsOpt = null) where T : JSettings => (T)_Load(file, typeof(T), useDefault, useDefaultOnError, jsOpt); static JSettings _Load(string file, Type type, bool useDefault, bool? useDefaultOnError, JsonSerializerOptions jsOpt) { if (file == null) return Activator.CreateInstance(type) as JSettings; jsOpt ??= SerializerOptions; JSettings R = null; file = pathname.normalize(file); if (filesystem.exists(file, true)) { try { if (useDefault) { filesystem.delete(file); } else { //using var p1 = perf.local(); //first time ~40 ms (similar hot and cold) var b = filesystem.loadBytes(file); //p1.Next('f'); R = JsonSerializer.Deserialize(b, type, jsOpt) as JSettings; //p1.Next('d'); R._loadedFile = true; } } catch (Exception ex) when (ex is not NotSupportedException) { if (!useDefault) { if (useDefaultOnError == false) throw; if (useDefaultOnError == true) { string backup = file + ".backup"; filesystem.move(file, backup, FIfExists.Delete); //note: don't try/catch print.it($"<>Failed to load settings from {file}. Will use default settings.\r\n\t<\a>{ex.ToStringWithoutStack()}\r\n\tBackup: {backup}<>"); } else { if (!Environment.UserInteractive) throw; int button = dialog.show("Failed to load settings", new($"{ex.ToStringWithoutStack()}\n\n{file}", o => { run.selectInExplorer(file); }), "1 Exit|2 Backup (rename) the file and use default settings", flags: DFlags.CommandLinks, icon: DIcon.Error); if (button == 1) Environment.Exit(1); else if (filesystem.exists(file)) filesystem.move(file, file + ".backup", FIfExists.Delete); } } } } R ??= Activator.CreateInstance(type) as JSettings; R._Ctor2(file, jsOpt); //autosave if (Interlocked.Exchange(ref s_loadedOnce, 1) == 0) { run.thread(() => { for (; ; ) { Thread.Sleep(2000); //Debug_.MemorySetAnchor(); _SaveAllIfNeed(true); //Debug_.MemoryPrint(); //editor ~6 KB } }, sta: false).Name = "Au.JSettings"; process.thisProcessExit += _ => { //info: .NET does not call finalizers when process exits FileWatcher.DisposeAll_(); _SaveAllIfNeed(false); s_list.Clear(); }; } lock (s_list) s_list.Add(R); return R; static void _SaveAllIfNeed(bool timer) { JSettings[] a; //can't lock both s_list and _lock (possible deadlock) lock (s_list) { a = s_list.ToArray(); } foreach (var v in a) { if (v.NoAutoSave) continue; if (timer && v.NoAutoSaveTimer) continue; v.SaveIfNeed(); } } } void _Ctor2(string file, JsonSerializerOptions jsOpt) { _file = file; _jsOpt = jsOpt; _old = JsonSerializer.SerializeToUtf8Bytes(this, GetType(), _jsOpt); } /// /// Saves now if need, and releases used resources. In the future will not save or reload. /// Don't need to call if the settings are used until process exit. /// public void Dispose() { Dispose(true); //no finalizer. Users can call Dispose, but usually settings objects live until process exit. } /// protected virtual void Dispose(bool disposing) { lock (s_list) if (!s_list.Remove(this)) return; //note: not inside `lock (_lock)` (possible deadlock) lock (_lock) { _watcher?.Dispose(); _watcher = null; _modifiedExternally = null; if (!NoAutoSave) SaveIfNeed(); _file = null; } } //repeated serialization speed with same options ~50 times better, eg 15000 -> 300 mcs cold. It's documented. Can be shared by multiple types. static readonly Lazy s_jsOptions = new(() => new() { AllowTrailingCommas = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, IncludeFields = true, IgnoreReadOnlyFields = true, IgnoreReadOnlyProperties = true, WriteIndented = true, }); /// /// Default deserialization and serialization options. /// /// /// /// public static JsonSerializerOptions SerializerOptions => s_jsOptions.Value; /// /// true if settings were loaded from file. /// /// /// Returns false if did not find the file (the settings were not saved) or failed to load/parse or parameter useDefault = true or parameter file = null. /// [JsonIgnore] public bool LoadedFile => _loadedFile; /// /// Don't automatically call . /// If false (default), calls every 2 s (unless true); also when disposing and when the process exits. /// protected bool NoAutoSave { get; set; } /// /// Don't call every 2 s. /// Default false. /// protected bool NoAutoSaveTimer { get; set; } /// /// Saves now if need. /// Call this when you want to save changed settings immediately; else the changes are auto-saved after max 2 s. See also and . /// public void SaveIfNeed() { lock (_lock) { if (_file is null) return; bool save = false; try { var b = JsonSerializer.SerializeToUtf8Bytes(this, GetType(), _jsOpt); save = !b.AsSpan().SequenceEqual(_old); if (save) { _watcher?.Paused = true; filesystem.saveBytes(_file, b); _old = b; } } catch (Exception ex) { print.it($"Failed to save settings to '{_file}'. {ex}"); } finally { if (save) _watcher?.Paused = false; } } } /// /// When detected that the settings file was externally modified or deleted (for example by another process of your program). /// /// /// Your event handler can call . /// /// Runs in a thread pool thread. /// /// Multiple objects in this process use the same file. public event Action ModifiedExternally { add { lock (_lock) { if (_file is null) return; _modifiedExternally += value ?? throw null; _watcher ??= FileWatcher.Watch(_file, _modifiedExternally); } } remove { lock (_lock) { _modifiedExternally -= value; if (_modifiedExternally == null) { _watcher?.Dispose(); _watcher = null; } } } } event Action _modifiedExternally; FileWatcher _watcher; /// /// Reloads settings from the file. /// /// /// Receives a new settings object that inherits the behavior of this object but contains updated values of fields defined in the derived class. If the file does not exist, the fields have default values. /// If failed, receives this object (unchanged). /// /// false if failed. /// /// Disposes this object. Does not change field values in it. It will no longer be tracked (e.g., for auto-save); tracking continues with the new object. /// /// /// Run this 2 times to test how a process of your app auto-reloads settings changed by another process of your app. /// { /// if (!sett.Reload(out sett)) return; /// print.it($"ModifiedExternally. This process: {process.thisProcessId}. {sett}"); /// }; /// /// var b = new wpfBuilder("JSettings example").WinSize(300).Columns(-1, -1, -1); /// b.R.AddButton("Print", _ => { print.it(sett); }); /// b.AddButton("one++", _ => { sett.one++; sett.SaveIfNeed(); }); /// b.AddButton("two++", _ => { sett.two++; sett.SaveIfNeed(); }); /// b.Window.Topmost = true; /// b.ShowDialog(); /// /// record Settings : JSettings { /// public static Settings Load() => Load(@"C:\Test\JSettings.json"); /// /// #pragma warning disable CS0649 /// public int one; /// public int two = 100; /// } /// ]]> /// public bool Reload(out T newSettings) where T : JSettings { bool ok = _Reload(out var js); newSettings = js as T; return ok; } bool _Reload(out JSettings newSettings) { newSettings = this; JSettings R; lock (_lock) { if (_file is null) { print.warning($"JSettings.Reload called for a disposed or momory-only settings object"); return false; } try { if (filesystem.exists(_file, true)) { var b = filesystem.loadBytes(_file); R = JsonSerializer.Deserialize(b, GetType(), _jsOpt) as JSettings; R._loadedFile = true; } else { R = Activator.CreateInstance(GetType()) as JSettings; } } catch (Exception ex) { print.warning($"Failed to reload settings from '{_file}'. {ex}", -1); return false; } R._Ctor2(_file, _jsOpt); R.NoAutoSave = NoAutoSave; R.NoAutoSaveTimer = NoAutoSaveTimer; R._watcher = _watcher; _watcher = null; R._modifiedExternally = _modifiedExternally; _modifiedExternally = null; _file = null; } lock (s_list) { s_list[s_list.IndexOf(this)] = R; } //note: not inside `lock (_lock)` (possible deadlock) newSettings = R; return true; } } ================================================ FILE: Au/Au.Types/TreeBase.cs ================================================ using System.Xml; namespace Au.Types; /// /// Base class for tree classes. /// The tree can be loaded/saved as XML. /// /// /// Implemented in the same way as . /// /// /// Shows how to declare a TreeBase-derived class, load tree of nodes from an XML file, find descendant nodes, save the tree to an XML file. /// { /// public string Name { get; set; } /// public int Id { get; private set; } /// public bool IsFolder { get; private set; } /// /// public MyTree(string name, int id, bool isFolder) { Name = name; Id = id; IsFolder = isFolder; } /// /// //XML element -> MyTree object /// MyTree(XmlReader x, MyTree parent) /// { /// if(parent == null) { //the root XML element /// if(x.Name != "example") throw new ArgumentException("XML root element must be 'example'"); /// IsFolder = true; /// } else { /// switch(x.Name) { /// case "e": break; /// case "f": IsFolder = true; break; /// default: throw new ArgumentException("XML element must be 'e' or 'f'"); /// } /// #if true //two ways of reading attributes /// Name = x["name"]; /// Id = x["id"].ToInt(); /// #else /// while(x.MoveToNextAttribute()) { /// var v = x.Value; /// switch(x.Name) { /// case "name": Name = v; break; /// case "id": Id = v.ToInt(); break; /// } /// } /// #endif /// if(Name.NE()) throw new ArgumentException("no 'name' attribute in XML"); /// if(Id == 0) throw new ArgumentException("no 'id' attribute in XML"); /// } /// } /// /// public static MyTree Load(string file) => XmlLoad(file, (x, p) => new MyTree(x, p)); /// /// public void Save(string file) => XmlSave(file, (x, n) => n._XmlWrite(x)); /// /// //MyTree object -> XML element /// void _XmlWrite(XmlWriter x) /// { /// if(Parent == null) { /// x.WriteStartElement("example"); /// } else { /// x.WriteStartElement(IsFolder ? "f" : "e"); /// x.WriteAttributeString("name", Name); /// x.WriteAttributeString("id", Id.ToString()); /// } /// } /// /// public override string ToString() => $"{new string(' ', Level)}{(IsFolder ? 'f' : 'e')} {Name} ({Id})"; /// } /// /// static void TNodeExample() { /// /* /// /// /// /// /// /// /// /// /// /// /// /// /// /// */ /// /// var x = MyTree.Load(@"C:\test\example.xml"); /// foreach(MyTree n in x.Descendants(true)) print.it(n); /// //print.it(x.Descendants().FirstOrDefault(k => k.Name == "seven")); //find a descendant /// //print.it(x.Descendants().Where(k => k.Level > 2)); //find some descendants /// x.Save(@"C:\test\example2.xml"); /// } /// ]]> /// public abstract class TreeBase where T : TreeBase { T _next; T _parent; T _lastChild; #region properties /// /// Returns the parent node. Can be null. /// public T Parent => _parent; /// /// Returns the root ancestor node. Its is null. /// Returns this node if its Parent is null. /// public T RootAncestor { get { var p = this as T; while (p._parent != null) p = p._parent; return p; } } /// /// Gets the number of ancestors (parent, its parent and so on). /// public int Level { get { int R = 0; for (var p = _parent; p != null; p = p._parent) R++; return R; } } /// /// Returns true if this node is a descendant of node n. /// /// Can be null. public bool IsDescendantOf(T n) { for (var p = _parent; p != null; p = p._parent) if (p == n) return true; return false; } /// /// Returns true if this node is a descendant of nearest ancestor node for which predicate returns true. /// public bool IsDescendantOf(Func predicate) { for (var p = _parent; p != null; p = p._parent) if (predicate(p)) return true; return false; } /// /// Returns true if this node is an ancestor of node n. /// /// Can be null. public bool IsAncestorOf(T n) => n?.IsDescendantOf(this as T) ?? false; /// /// Returns true if is not null. /// public bool HasParent => _parent != null; /// /// Returns true if this node has child nodes. /// public bool HasChildren => _lastChild != null; /// /// Gets the last child node, or null if none. /// public T LastChild => _lastChild; /// /// Gets the first child node, or null if none. /// public T FirstChild => _lastChild?._next; /// /// Gets next sibling node, or null if none. /// public T Next => _parent == null || this == _parent._lastChild ? null : _next; /// /// Gets previous sibling node, or null if none. /// /// /// Can be slow if there are many siblings. This class does not have a "previous" field and therefore has to walk the linked list of siblings. /// public T Previous { get { if (_parent == null) return null; T n = _parent._lastChild._next; Debug.Assert(n != null); T p = null; while (n != this) { p = n; n = n._next; } return p; } } /// /// Returns 0-based index of this node in parent. /// Returns -1 if no parent. /// /// /// Can be slow if there are many siblings. This class does not have an "index" field and therefore has to walk the linked list of siblings. /// public int Index { get { var p = _parent; if (p != null) { var n = p._lastChild; for (int i = 0; ; i++) { n = n._next; if (n == this) return i; } } return -1; } } #endregion #region methods void _AddCommon(T n) { if (n == null || n._parent != null || n == RootAncestor) throw new ArgumentException(); n._parent = this as T; } /// /// Adds node n to this node as a child. /// /// /// Insert n as the first child node. If false (default), appends to the end. /// n is null, or has parent (need to at first), or is this node, or an ancestor of this node. public void AddChild(T n, bool first = false) { _AddCommon(n); if (_lastChild == null) { //our first child! n._next = n; //n now is LastChild and FirstChild } else { n._next = _lastChild._next; //_next of _lastChild is FirstChild _lastChild._next = n; if (first) return; } _lastChild = n; } /// /// Inserts node n before or after this node as a sibling. /// /// /// Insert n after this node. If false (default), inserts before this node. /// See . /// This node does not have parent ( is null). public void AddSibling(T n, bool after) { if (_parent == null) throw new InvalidOperationException("no parent"); _parent._Insert(n, this as T, after); } void _Insert(T n, T anchor, bool after) { if (after && anchor == _lastChild) { //after last child AddChild(n); } else if (!after && anchor == _lastChild._next) { //before first child AddChild(n, true); } else { _AddCommon(n); T prev, next; if (after) { prev = anchor; next = anchor._next; } else { prev = anchor.Previous; next = anchor; } n._next = next; prev._next = n; } } /// /// Removes this node from its parent. /// /// /// After removing, the property is null. /// Does nothing if Parent is null. /// public void Remove() => _parent?._Remove(this as T); void _Remove(T n) { Debug.Assert(n?._parent == this); T p = _lastChild; while (p._next != n) p = p._next; if (p == n) { _lastChild = null; } else { if (_lastChild == n) _lastChild = p; p._next = n._next; } n._parent = null; n._next = null; } /// /// Gets ancestor nodes. The order is from this node towards the root node. /// /// Include this node. /// Don't include . public IEnumerable Ancestors(bool andSelf = false, bool noRoot = false) { var n = andSelf ? this as T : _parent; while (n != null) { if (noRoot && n._parent == null) break; yield return n; n = n._parent; } } /// /// Gets ancestor nodes. The order is from the root node towards this node. /// /// Include this node. Default false. /// Don't include . public T[] AncestorsFromRoot(bool andSelf = false, bool noRoot = false) { T nFrom = andSelf ? this as T : _parent; //count int len = 0; for (var n = nFrom; n != null; n = n._parent) { if (noRoot && n._parent == null) break; len++; } //array if (len == 0) return []; var a = new T[len]; for (var n = nFrom; len > 0; n = n._parent) a[--len] = n; return a; //info: can use LINQ Reverse, but this func makes less garbage. } /// /// Gets all direct child nodes. /// /// Include this node. Default false. public IEnumerable Children(bool andSelf = false) { if (andSelf) yield return this as T; if (_lastChild != null) { var n = _lastChild; do { n = n._next; yield return n; } while (n != _lastChild); } } /// /// Gets number of direct child nodes. /// public int Count { get { int r = 0; if (_lastChild != null) { var n = _lastChild; do { r++; } while ((n = n._next) != _lastChild); } return r; } } /// /// Gets all descendant nodes (direct children, their children and so on). /// /// Include this node. Default false. /// If not null, called for each descendant node that has children. Let it return false to skip descendants of that node. public IEnumerable Descendants(bool andSelf = false, Func stepInto = null) { var n = this as T; if (andSelf) yield return n; while (true) { T last = n._lastChild; if (last != null && !(stepInto is { } si && n != this && !si(n))) { n = last._next; } else { while (n != null && n != this && n == n._parent._lastChild) n = n._parent; if (n == null || n == this) break; n = n._next; } yield return n; } } #endregion #region XML /// /// Used with /// protected delegate T XmlNodeReader(XmlReader x, T parent); /// /// Used with /// protected delegate void XmlNodeWriter(XmlWriter x, T node); /// /// Loads XML file and creates tree of nodes from it. /// /// the root node. /// XML file. Must be full path. Can contain environment variables etc, see . /// Callback function that reads current XML element and creates/returns new node. See example. /// Not full path. /// Exceptions of . /// An error occurred while parsing the XML. /// protected static T XmlLoad(string file, XmlNodeReader nodeReader) { file = pathname.NormalizeMinimally_(file); var xs = new XmlReaderSettings() { IgnoreComments = true, IgnoreProcessingInstructions = true, IgnoreWhitespace = true }; using var r = filesystem.waitIfLocked(() => XmlReader.Create(file, xs)); return XmlLoad(r, nodeReader); } /// /// Reads XML and creates tree of nodes. /// /// the root node. /// /// Callback function that reads current XML element and creates/returns new node. /// An error occurred while parsing the XML. /// protected static T XmlLoad(XmlReader x, XmlNodeReader nodeReader) { Not_.Null(x, nodeReader); T root = null, parent = null; while (x.Read()) { var nodeType = x.NodeType; if (nodeType == XmlNodeType.Element) { var n = nodeReader(x, parent); if (root == null) root = n; else parent.AddChild(n); x.MoveToElement(); if (!x.IsEmptyElement) parent = n; } else if (nodeType == XmlNodeType.EndElement) { if (parent == null) break; if (parent == root) break; parent = parent._parent; } } return root; } /// /// Saves tree of nodes (this and descendants) to an XML file. /// /// XML file. Must be full path. Can contain environment variables etc, see . /// Callback function that writes node's XML start element (see ) and attributes (see ). Must not write children and end element. Also should not write value, unless your reader knows how to read it. /// XML formatting settings. Optional. /// If not null, writes these nodes as if they were children of this node. /// Not full path. /// Exceptions of and other methods. /// /// Uses . It ensures that existing file data is not damaged on exception etc. /// /// protected void XmlSave(string file, XmlNodeWriter nodeWriter, XmlWriterSettings sett = null, IEnumerable children = null) { file = pathname.NormalizeMinimally_(file); sett ??= new XmlWriterSettings() { OmitXmlDeclaration = true, Indent = true, IndentChars = " " }; filesystem.save(file, temp => { using var x = XmlWriter.Create(temp, sett); XmlSave(x, nodeWriter, children); }); } /// /// Writes tree of nodes (this and descendants) to an . /// /// Exceptions of methods. /// /// protected void XmlSave(XmlWriter x, XmlNodeWriter nodeWriter, IEnumerable children = null) { Not_.Null(x, nodeWriter); x.WriteStartDocument(); if (children == null) { _XmlWrite(x, nodeWriter); } else { nodeWriter(x, this as T); foreach (var n in children) n._XmlWrite(x, nodeWriter); x.WriteEndElement(); } x.WriteEndDocument(); } void _XmlWrite(XmlWriter x, XmlNodeWriter nodeWriter) { nodeWriter(x, this as T); if (_lastChild != null) { var c = _lastChild; do { c = c._next; c._XmlWrite(x, nodeWriter); } while (c != _lastChild); } x.WriteEndElement(); } #endregion } ================================================ FILE: Au/Au.Types/common.cs ================================================ using System.Diagnostics.CodeAnalysis; using static Au.More.Serializer_; namespace Au.Types { /// /// In DocFX-generated help files removes documentation and auto-generated links in TOC and class pages. For it is used filter.yml. /// [EditorBrowsable(EditorBrowsableState.Never), AttributeUsage(AttributeTargets.All)] public sealed class NoDoc : Attribute { } //tested: the DocFX /// removes the member from the compilation, not just prevents creating the doc article. Then may be error; don't use it. See https://dotnet.github.io/docfx/docs/dotnet-api-docs.html /// /// If a class is derived from this class, editor adds undeclared Windows API to its completion list. /// public abstract class NativeApi { //Or for it could use an attribute. But this base class easily solves 2 problems: // 1. In 'new' expression does not show completion list (with types from winapi DB) if the winapi class still does not have types inside. Because the completion service then returns null. Now it is solved, because this class has nested types. // 2. If class with attributes is after top-level statements, code info often does not work when typing directly above it. Works better if without attributes. //FUTURE: Also add attribute. Then can specify an alternative DB or text file with declarations. Maybe also some settings. // But use this class as base too, like now. Eg could add protected util functions. Could use this class as both (base and attribute), but Attribute has static members. #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member /// /// Can be used in structures as flexible array member (the last field, defined like Type[1] name; in C). /// public struct FlexibleArray where T : unmanaged { T _0; public ref T this[int index] { [UnscopedRef] [MethodImpl(MethodImplOptions.AggressiveInlining)] get => ref Unsafe.Add(ref _0, index); } [UnscopedRef] [MethodImpl(MethodImplOptions.AggressiveInlining)] public Span AsSpan(int length) { return MemoryMarshal.CreateSpan(ref _0, length); } } /// /// Windows API BOOL, with implicit conversions to/from C# bool. /// public readonly record struct BOOL { public readonly int IntValue; public BOOL(bool b) { IntValue = b ? 1 : 0; } public static implicit operator bool(BOOL b) => b.IntValue != 0; public static implicit operator BOOL(bool b) => new(b); public override string ToString() => IntValue == 0 ? "False" : "True"; } #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member } /// /// Invokes specified action (calls callback function) at the end of using(...) { ... }. /// Usually returned by functions. Example: . /// public struct UsingEndAction : IDisposable { readonly Action _a; /// Sets action to be invoked when disposing this variable. public UsingEndAction(Action a) => _a = a; /// Invokes the action if not null. public void Dispose() => _a?.Invoke(); } /// /// Used with to specify string parameter format. /// public enum PSFormat { /// None, /// /// Keys. See . /// Keys, /// /// [Wildcard expression](xref:wildcard_expression). /// Wildex, /// /// PCRE regular expression. /// Regexp, /// /// PCRE regular expression replacement string. /// RegexpReplacement, /// /// .NET regular expression. /// NetRegex, /// /// Hotkey, except triggers. /// Hotkey, /// /// Hotkey trigger. /// HotkeyTrigger, /// /// Trigger modifiers without key. /// TriggerMod, /// /// Name or path of a script or class file in current workspace. /// CodeFile, /// /// Name or path of any file or folder in current workspace. /// FileInWorkspace, } /// /// A function parameter with this attribute is a string of the specified format, for example regular expression. /// Code editors should help to create correct string arguments: provide tools or reference, show errors. /// [AttributeUsage(AttributeTargets.Parameter /*| AttributeTargets.Field | AttributeTargets.Property*/, AllowMultiple = false)] public sealed class ParamStringAttribute : Attribute { //info: now .NET has similar attribute StringSyntaxAttribute. It was added later. /// public ParamStringAttribute(PSFormat format) => Format = format; /// public PSFormat Format { get; set; } } ///// ///// Specifies whether to set, add or remove flags. ///// //public enum SetAddRemove //{ // /// Set flags = the specified value. // Set = 0, // /// Add the specified flags, don't change others. // Add = 1, // /// Remove the specified flags, don't change others. // Remove = 2, // /// Toggle the specified flags, don't change others. // Xor = 3, //} } namespace System.Runtime.CompilerServices //the attribute must be in this namespace { /// [NoDoc] [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] public class IgnoresAccessChecksToAttribute : Attribute { /// public IgnoresAccessChecksToAttribute(string assemblyName) { AssemblyName = assemblyName; } /// public string AssemblyName { get; } } } ================================================ FILE: Au/Au.Types/exceptions.cs ================================================ namespace Au.Types { /// /// The base exception class used in this library. /// Thrown when something fails and there is no better exception type for that failure. /// /// /// Some constructors support Windows API error code. Then will contain its error description. /// If the string passed to the constructor starts with "*", replaces the "*" with "Failed to ". If does not end with ".", appends ".". /// public class AuException : Exception { /// /// Sets = message (default "Failed."). /// Sets = 0. /// public AuException(string message = "Failed.", Exception innerException = null) : base(message, innerException) { } /// /// Sets = (errorCode != 0) ? errorCode : lastError.code. /// Sets = message + " " + lastError.messageFor(NativeErrorCode). /// public AuException(int errorCode, string message = "Failed.", Exception innerException = null) : base(message, innerException) { NativeErrorCode = (errorCode != 0) ? errorCode : lastError.code; } /// Gets the Windows API error code. public int NativeErrorCode { get; protected set; } /// Gets error message. public override string Message => FormattedMessage ?? FormatMessage(); /// String created by , which should be called by the override if null. Initially null. protected string FormattedMessage; /// /// Formats error message. Sets and returns . /// /// /// As base text, uses the text passed to the constructor (default "Failed."). /// If it starts with "*", replaces the "*" with "Failed to ". /// If it ends with "*", replaces the "*" with commonPostfix if it is not empty. /// If then the message does not end with ".", appends ".". /// If appendMessage is null, uses lastError.messageFor(NativeErrorCode) if not 0. /// If then appendMessage is not empty, appends " " + appendMessage. /// Also appends InnerException.Message in new tab-indented line if is not null. /// protected string FormatMessage(string appendMessage = null, string commonPostfix = null) { var m = base.Message; if (!m.NE()) { if (m[0] == '*') m = "Failed to " + m[1..]; if (!commonPostfix.NE() && m[^1] == '*') m = m[..^1] + commonPostfix; if (!m.Ends('.')) m += "."; } if (appendMessage == null && NativeErrorCode != 0) appendMessage = lastError.messageFor(NativeErrorCode); if (!appendMessage.NE()) m = m + " " + appendMessage; if (InnerException != null) m = m + "\r\n\t" + InnerException.Message; return FormattedMessage = m; } /// /// If errorCode is not 0, throws that includes the code and its message. /// More info: . /// /// Windows API error code or HRESULT. /// Main message. The message of the error code will be appended to it. public static void ThrowIfHresultNot0(int errorCode, string message = null) { if (errorCode != 0) throw new AuException(errorCode, message); } /// /// If errorCode is less than 0, throws that includes the code and its message. /// More info: . /// /// Windows API error code or HRESULT. /// Main message. The message of the error code will be appended to it. public static void ThrowIfHresultNegative(int errorCode, string message = null) { if (errorCode < 0) throw new AuException(errorCode, message); } } /// /// Exception thrown mostly by functions. /// /// /// Some constructors support Windows API error code. Then also will contain its error description. /// If error code ERROR_INVALID_WINDOW_HANDLE, also depends on whether the window handle is 0. /// If parameter errorCode is 0 or not used: if the window handle is invalid, uses ERROR_INVALID_WINDOW_HANDLE. /// If the string passed to the constructor starts with "*", replaces the "*" with "Failed to ". If ends with "*", replaces the "*" with " window.". If does not end with ".", appends ".". /// public class AuWndException : AuException { const string _errStr_0Handle = "The window handle is 0. Usually it means 'window not found'."; const string _errStr_InvalidHandle = "Invalid window handle. Usually it means 'the window was closed'."; /// /// Sets = message (default "Failed."). /// Sets = w.IsAlive ? 0 : ERROR_INVALID_WINDOW_HANDLE. /// public AuWndException(wnd w, string message = "Failed.", Exception innerException = null) : base(message, innerException) { Window = w; NativeErrorCode = _Code(0, w); } /// /// Sets = (errorCode != 0) ? errorCode : (w.IsAlive ? lastError.code : ERROR_INVALID_WINDOW_HANDLE). /// Sets = message + " " + lastError.messageFor(NativeErrorCode). /// public AuWndException(wnd w, int errorCode, string message = "Failed.", Exception innerException = null) : base(_Code(errorCode, w), message, innerException) { Window = w; } static int _Code(int code, wnd w) { if (code != 0 || w.IsAlive) return code; return Api.ERROR_INVALID_WINDOW_HANDLE; } /// Gets the window passed to the constructor. public wnd Window { get; } /// Gets error message. public override string Message { get { if (FormattedMessage == null) { string m; if (Window.Is0) m = _errStr_0Handle; else if (NativeErrorCode == Api.ERROR_INVALID_WINDOW_HANDLE) m = _errStr_InvalidHandle; else m = null; //will append lastError.messageFor(NativeErrorCode) if NativeErrorCode not 0, or InnerException.Message if it is not null. FormatMessage(m, " window."); } return FormattedMessage; } } } /// /// Functions that search for an object can throw this exception when not found. /// public class NotFoundException : Exception { /// /// Sets Message = "Not found.". /// public NotFoundException() : base("Not found.") { } /// /// Sets Message = message. /// public NotFoundException(string message) : base(message) { } } ///// ///// Exception thrown when a callback function returns an invalid value. ///// //public class BadCallbackException : Exception { // /// // /// Sets Message = "The callback function returned an invalid value.". // /// // public BadCallbackException() : base("The callback function returned an invalid value.") { } // /// // /// Sets Message = message. // /// // public BadCallbackException(string message) : base(message) { } //} /// /// This exception is thrown when current thread is not on the input desktop and therefore cannot use mouse, keyboard, clipboard and window functions. /// For example when PC locked (Win+L), screen saver, UAC consent, Ctrl+Alt+Delete or not in the active user session. /// /// public class InputDesktopException : AuException { /// Message text before the standard message text of this exception. public InputDesktopException(string message = null) : base(message.NE() ? c_message : message + (message.Ends('.') ? " " : ". ") + c_message) { } const string c_message = "The main input desktop is inactive; mouse, keyboard, clipboard and UI functions are disabled."; /// /// Calls . If it returns false, throws . /// /// Message text before the standard message text of this exception. /// public static void ThrowIfBadDesktop(string message = null, bool detectLocked = false) { if (!miscInfo.isInputDesktop(detectLocked)) throw new InputDesktopException(message); } } } namespace Au.Types { static partial class ExtMisc { /// /// Returns string containing exception type name and message. /// public static string ToStringWithoutStack(this Exception t) { return t.GetType().Name + ", " + t.Message; } } } ================================================ FILE: Au/Au.Types/param types.cs ================================================ namespace Au.Types; #if false //test docfx preprocessing /// /// Sum. /// two /// /// A. /// B. public record class PublicRecord(int A, int B); record class InternalRecord_ { public int A { get; set; } public int B { get; set; } } record class InternalRecord2_(int A, int B) { public int C { get; set; } } record class InternalRecord3_(int A, int B); public class NormalClass { record class _Record4(int A, int B) { public int C { get; set; } } /// /// Sum. /// /// Param. public void Meth(int i) { } } #endif /// /// Contains x or y coordinate in screen or some other rectangle that can be specified in various ways: normal, reverse, fraction, center, max. /// Used for parameters of functions like , . /// /// /// To specify a normal coordinate (the origin is the left or top edge), assign an int value (implicit conversion from int to Coord). /// To specify a reverse coordinate (the origin is the right or bottom edge), use or a "from end" index like ^1. It is towards the left or top edge, unless negative. Or use or . /// To specify a "fraction of the rectangle" coordinate, use or a value of type float like .5f. Or use . /// The meaning of default(Coord) depends on function where used. Many functions interpret it as center (same as Coord.Center or .5f). /// Also there are functions to convert Coord to normal coordinates. /// /// /// /// public record struct Coord { //Use single long field that packs int and CoordType. //If 2 fields (int and CoordType), 64-bit compiler creates huge calling code. //This version is good in 32-bit, very good in 64-bit. Even better than packing in single int (30 bits value and 2 bits type). //Don't use struct or union with both int and float fields. It creates slow and huge calling code. readonly long _v; /// /// Value type. /// public CoordType Type => (CoordType)(_v >> 32); /// /// Non-fraction value. /// public int Value => (int)_v; /// /// Fraction value. /// public unsafe float FractionValue => BitConverter.Int32BitsToSingle((int)_v); /// /// Returns true if Type == CoordType.None (no value assigned). /// public bool IsEmpty => Type == CoordType.None; Coord(CoordType type, int value) { _v = ((long)type << 32) | (uint)value; } //info: if type and value are constants, compiler knows the final value and does not use the << and | operators in the compiled code. /// /// Creates of Normal type. /// //[MethodImpl(MethodImplOptions.NoInlining)] //makes bigger/slower public static implicit operator Coord(int v) => new(CoordType.Normal, v); /// /// Creates of Normal or Reverse type. Reverse if the index is from end, like ^1. /// public static implicit operator Coord(Index v) => new(v.IsFromEnd ? CoordType.Reverse : CoordType.Normal, v.Value); /// /// Creates of Fraction type. /// public static implicit operator Coord(float v) => new(CoordType.Fraction, BitConverter.SingleToInt32Bits(v)); //these would create Fraction /// [Obsolete("The value must be of type int or float.", error: true), NoDoc] public static implicit operator Coord(uint f) => default; /// [Obsolete("The value must be of type int or float.", error: true), NoDoc] public static implicit operator Coord(long f) => default; /// [Obsolete("The value must be of type int or float.", error: true), NoDoc] public static implicit operator Coord(ulong f) => default; //tested: compiler does not allow to assign nint. /// /// Creates of Reverse type. /// Value 0 is at the right or bottom, and does not belong to the rectangle. Positive values are towards left or top. /// /// /// Instead can be use "from end" index, for example argument Coord.Reverse(1) can be replaced with ^1. /// public static Coord Reverse(int v) => new(CoordType.Reverse, v); /// /// Creates of Fraction type. /// Value 0 is the left or top of the rectangle. Value 1.0 is the right or bottom of the rectangle. Values <0 and >=1.0 are outside of the rectangle. /// /// /// Instead can be used implicit conversion from float, for example argument Coord.Fraction(.5) can be replaced with .5f. /// public static unsafe Coord Fraction(double v) => (float)v; /// /// Returns Fraction(0.5). /// /// public static Coord Center => .5f; /// /// Returns Reverse(0). Same as ^0. /// This point will be outside of the rectangle. See also . /// /// public static Coord Max => Reverse(0); /// /// Returns Reverse(1). Same as ^1. /// This point will be inside of the rectangle, at the very right or bottom, assuming the rectangle is not empty. /// /// public static Coord MaxInside => Reverse(1); //rejected: this could be used like Coord.Max + 1. Too limited usage. //public static Coord operator +(Coord c, int i) { return ...; } static bool _NeedRect(Coord x, Coord y) { return (x.Type > CoordType.Normal) || (y.Type > CoordType.Normal); } /// /// Converts fractional/reverse coordinate to normal coordinate in a range. /// /// Start of range. /// End of range. public int NormalizeInRange(int start, int end) { return Type switch { CoordType.Normal => start + Value, CoordType.Reverse => end - Value, CoordType.Fraction => start + (int)((end - start) * FractionValue), _ => 0, }; } /// /// Converts fractional/reverse coordinates to normal coordinates in a rectangle. /// /// X coordinate relative to r. /// Y coordinate relative to r. /// The rectangle. /// Use only width and height of r. If false (default), the function adds r offset (left and top). /// If x or y is default, use . Not used with widthHeight. public static POINT NormalizeInRect(Coord x, Coord y, RECT r, bool widthHeight = false, bool centerIfEmpty = false) { if (widthHeight) r.Move(0, 0); else if (centerIfEmpty) { if (x.IsEmpty) x = Center; if (y.IsEmpty) y = Center; } return (x.NormalizeInRange(r.left, r.right), y.NormalizeInRange(r.top, r.bottom)); } /// /// Returns normal coordinates relative to the client area of a window. Converts fractional/reverse coordinates etc. /// /// X coordinate relative to the client area of w. /// Y coordinate relative to the client area of w. /// The window. /// x y are relative to the entire w rectangle, not to its client area. /// If x or y is default, use . public static POINT NormalizeInWindow(Coord x, Coord y, wnd w, bool nonClient = false, bool centerIfEmpty = false) { //info: don't need widthHeight parameter because client area left/top are 0. With non-client don't need in this library and probably not useful. But if need, caller can explicitly offset the rect before calling this func. if (centerIfEmpty) { if (x.IsEmpty) x = Center; if (y.IsEmpty) y = Center; } POINT p = default; if (!x.IsEmpty || !y.IsEmpty) { RECT r; if (nonClient) { w.GetRectIn(w, out r); } else if (_NeedRect(x, y)) { r = w.ClientRect; } else r = default; p.x = x.NormalizeInRange(r.left, r.right); p.y = y.NormalizeInRange(r.top, r.bottom); } return p; } /// /// Returns normal coordinates relative to the primary screen. Converts fractional/reverse coordinates etc. /// /// X coordinate relative to the specified screen (default - primary). /// Y coordinate relative to the specified screen (default - primary). /// x y are relative to the work area. /// If used, x y are relative to this screen. Default - primary screen. Example: screen.index(1). /// Use only width and height of the screen rectangle. If false, the function adds its offset (left and top, which can be nonzero if using the work area or a non-primary screen). /// If x or y is default, use . public static POINT Normalize(Coord x, Coord y, bool workArea = false, screen screen = default, bool widthHeight = false, bool centerIfEmpty = false) { if (centerIfEmpty) { if (x.IsEmpty) x = Center; if (y.IsEmpty) y = Center; } POINT p = default; if (!x.IsEmpty || !y.IsEmpty) { RECT r; if (workArea || !screen.IsEmpty || _NeedRect(x, y)) { r = screen.GetRect(workArea); if (widthHeight) r.Move(0, 0); } else r = default; p.x = x.NormalizeInRange(r.left, r.right); p.y = y.NormalizeInRange(r.top, r.bottom); } return p; } /// public override string ToString() { switch (Type) { case CoordType.Normal: return Value.ToString() + ", Normal"; case CoordType.Reverse: return Value.ToString() + ", Reverse"; case CoordType.Fraction: return FractionValue.ToS() + ", Fraction"; default: return "default"; } } } /// /// variable value type. /// public enum CoordType { /// /// No value. The variable is default(Coord). /// None, /// /// is pixel offset from left or top of a rectangle. /// Normal, /// /// is pixel offset from right or bottom of a rectangle, towards left or top. /// Reverse, /// /// is fraction of a rectangle, where 0.0 is left or top, and 1.0 is right or bottom (outside of the rectangle). /// Fraction, } /// /// Can be used to specify coordinates for various popup windows, like new PopupXY(x, y), (x, y), PopupXY.In(rectangle), PopupXY.Mouse. /// public class PopupXY { #pragma warning disable 1591 //XML doc public Coord x, y; public screen screen; public bool workArea; public bool inRect; public RECT rect; #pragma warning restore 1591 //XML doc /// /// Sets position and/or screen. /// /// X relative to the screen or work area. Default - center. /// X relative to the screen or work area. Default - center. /// x y are relative to the work area of the screen. /// Can be used to specify a screen. Default - primary. Example: screen.index(1). /// /// Also there is are implicit conversions from tuple (x, y) and . Instead of new PopupXY(x, y) you can use (x, y). Instead of new PopupXY(p.x, p.y, false) you can use p or (POINT)p . /// public PopupXY(Coord x = default, Coord y = default, bool workArea = true, screen screen = default) { this.x = x; this.y = y; this.workArea = workArea; this.screen = screen; } /// /// Creates new that specifies position in a rectangle. For example of the owner window. /// /// Rectangle relative to the primary screen. /// X relative to the rectangle. Default - center. /// Y relative to the rectangle. Default - center. public static PopupXY In(RECT r, Coord x = default, Coord y = default) => new(x, y) { inRect = true, rect = r }; /// /// Creates new that specifies position relative to the work area of the primary screen. /// public static implicit operator PopupXY((Coord x, Coord y) p) => new(p.x, p.y, true); /// Creates new that specifies position relative to the primary screen (not to the work area). public static implicit operator PopupXY(POINT p) => new(p.x, p.y, false); //info: this conversion can be used with PopupXY.Mouse. //public bool IsRawXY => !inRect && screen.IsNull && workArea == false && x.Type == Coord.CoordType.Normal && y.Type == Coord.CoordType.Normal; /// /// Gets point coordinates below mouse cursor, for showing a tooltip-like popup. /// public static POINT Mouse { get { var p = mouse.xy; var scr = screen.of(p); var rs = scr.Rect; int dy = Dpi.Scale(100, scr.Dpi); if (rs.bottom - p.y < dy) return (p.x, p.y - dy); int cy = Dpi.GetSystemMetrics(Api.SM_CYCURSOR, p); if (MouseCursor.GetCurrentVisibleCursor(out var c) && Api.GetIconInfo(c, out var u)) { if (u.hbmColor != default) Api.DeleteObject(u.hbmColor); Api.DeleteObject(u.hbmMask); //print.it(u.xHotspot, u.yHotspot); p.y += cy - u.yHotspot - 1; //not perfect, but better than just to add SM_CYCURSOR or some constant value. return p; } return (p.x, p.y + cy - 5); } } /// /// Gets if not empty, else screen that contains the specified point. /// public screen GetScreen() { if (!screen.IsEmpty) return screen.Now; POINT p = inRect ? Coord.NormalizeInRect(x, y, rect, centerIfEmpty: true) : Coord.Normalize(x, y, workArea); return screen.of(p); } } /// /// Used for parameters of functions that need a window handle as but also accept a WPF window/element, winforms form/control and IntPtr. /// /// /// Has implicit conversions from: ///
- WPF window or element. ///
- Form or control. ///
IntPtr or nint - window handle. ///
public struct AnyWnd { readonly object _o; AnyWnd(object o) { _o = o; } /// Assignment of a value of type . public static implicit operator AnyWnd(wnd w) => new(w); /// Assignment of a window handle as IntPtr. public static implicit operator AnyWnd(IntPtr hwnd) => new((wnd)hwnd); /// Assignment of a value of type (Form or any control class). public static implicit operator AnyWnd(System.Windows.Forms.Control c) => new(c); /// Assignment of a value of type System.Windows.DependencyObject (WPF window or control). public static implicit operator AnyWnd(System.Windows.DependencyObject c) => c != null ? new AnyWnd(new object[] { c }) : default; /// /// Gets the window or control handle as . /// /// default(wnd) if not assigned. public wnd Hwnd => wnd.Internal_.FromObject(_o); /// /// true if this is default(AnyWnd). /// public bool IsEmpty => _o == null; } /// /// Used for function parameters to specify multiple strings. /// Contains a string like "One|Two|Three" or string[] or List<string>. Has implicit conversions from these types. Can be assigned collection initializer like ["a", "b"]. /// [CollectionBuilder(typeof(Strings), "Create")] public struct Strings : IEnumerable { readonly object _o; Strings(object o) { _o = o; } /// public Strings(params string[] a) { _o = a; } /// public static implicit operator Strings(string s) => new((object)s); /// public static implicit operator Strings(string[] e) => new(e); /// public static implicit operator Strings(List e) => new(e); /// /// The raw value. /// public object Value => _o; /// /// Converts the value to string[]. /// Note: don't modify array elements. If the caller passed an array, this function returns it, not a copy. /// public string[] ToArray() { return _o switch { string[] a => a, string s => s.Split('|'), List a => a.ToArray(), _ => [], //null }; } #region support collection expression //IEnumerable IEnumerator IEnumerable.GetEnumerator() => ToArray().AsEnumerable().GetEnumerator(); //IEnumerable IEnumerator IEnumerable.GetEnumerator() => ToArray().GetEnumerator(); /// /// Returns new(span.ToArray()). /// public static Strings Create(ReadOnlySpan span) => new(span.ToArray()); #endregion } /// /// Font name, size and style. /// If Name not set, will be used standard GUI font; then Size can be 0 to use size of standard GUI font. /// On high-DPI screen the font size will be scaled. /// public record class FontNSS(int Size = 0, string Name = null, bool Bold = false, bool Italic = false) { /// /// Creates font. /// /// DPI for scaling. internal NativeFont_ CreateFont(DpiOf dpi) { if (Name == null) return new(dpi, Bold, Italic, Size); return new(dpi, Name, Size, Bold, Italic); } } ================================================ FILE: Au/Au.Types/structs.cs ================================================ using System.Drawing; using System.Text.Json.Serialization; namespace Au.Types { /// /// Point coordinates x y. /// public record struct POINT { #pragma warning disable 1591, 3008 //XML doc, CLS-compliant [JsonInclude] public int x, y; public POINT(int x, int y) { this.x = x; this.y = y; } public static implicit operator POINT((int x, int y) t) => new(t.x, t.y); public static implicit operator POINT(Point p) => new(p.X, p.Y); public static implicit operator Point(POINT p) => new(p.x, p.y); public static implicit operator PointF(POINT p) => new(p.x, p.y); public static implicit operator System.Windows.Point(POINT p) => new(p.x, p.y); /// /// Can round up, for example 1.7 to 2. /// public static POINT From(PointF p, bool round) => new(round ? p.X.ToInt() : checked((int)p.X), round ? p.Y.ToInt() : checked((int)p.Y)); /// /// Can round up, for example 1.7 to 2. /// public static POINT From(System.Windows.Point p, bool round) => new(round ? p.X.ToInt() : checked((int)p.X), round ? p.Y.ToInt() : checked((int)p.Y)); //rejected ///// Specifies position relative to the primary screen or its work area. Calls with centerIfEmpty true. //public static implicit operator POINT((Coord x, Coord y, bool workArea) t) => _Coord(t.x, t.y, t.workArea, default); ///// Specifies position relative to the specified screen or its work area. Calls with centerIfEmpty true. //public static implicit operator POINT((Coord x, Coord y, screen screen, bool workArea) t) => _Coord(t.x, t.y, t.workArea, t.screen); ///// Specifies position in the specified rectangle which is relative to the primary screen. Calls with centerIfEmpty true. //public static implicit operator POINT((RECT r, Coord x, Coord y) t) => Coord.NormalizeInRect(t.x, t.y, t.r, centerIfEmpty: true); //static POINT _Coord(Coord x, Coord y, bool workArea, screen screen) => Coord.Normalize(x, y, workArea, screen, centerIfEmpty: true); //maybe in the future ///// ///// Converts coordinates into real coordinates. ///// Calls with centerIfEmpty true. ///// //public static POINT Normalize(Coord x, Coord y, bool workArea = false, screen screen = default) // => Coord.Normalize(x, y, workArea, screen, centerIfEmpty: true); //public static POINT NormalizeIn(RECT r, Coord x = default, Coord y = default) // => Coord.NormalizeInRect(x, y, r, centerIfEmpty: true); /// this.x += x; this.y += y; public void Offset(int x, int y) { this.x += x; this.y += y; } /// Returns new POINT(p.x + d.x, p.y + d.y). public static POINT operator +(POINT p, (int x, int y) d) => new(p.x + d.x, p.y + d.y); public void Deconstruct(out int x, out int y) { x = this.x; y = this.y; } public override string ToString() => $"{{{x.ToS()}, {y.ToS()}}}"; #pragma warning restore 1591 //XML doc } /// /// Width and height. /// public record struct SIZE { #pragma warning disable 1591, 3008 //XML doc, CLS-compliant [JsonInclude] public int width, height; public SIZE(int width, int height) { this.width = width; this.height = height; } public static implicit operator SIZE((int width, int height) t) => new(t.width, t.height); public static implicit operator SIZE(Size z) => new(z.Width, z.Height); public static implicit operator Size(SIZE z) => new(z.width, z.height); public static implicit operator SizeF(SIZE z) => new(z.width, z.height); public static implicit operator System.Windows.Size(SIZE z) => new(z.width, z.height); /// /// Can round up, for example 1.7 to 2. /// public static SIZE From(SizeF z, bool round) => new(round ? z.Width.ToInt() : checked((int)z.Width), round ? z.Height.ToInt() : checked((int)z.Height)); /// /// Can round up, for example 1.7 to 2. /// public static SIZE From(System.Windows.Size z, bool round) => new(round ? z.Width.ToInt() : checked((int)z.Width), round ? z.Height.ToInt() : checked((int)z.Height)); /// Returns new SIZE(z.width + d.x, z.height + d.y). public static SIZE operator +(SIZE z, (int x, int y) d) => new(z.width + d.x, z.height + d.y); public void Deconstruct(out int width, out int height) { width = this.width; height = this.height; } public override string ToString() => $"{{{width.ToS()}, {height.ToS()}}}"; #pragma warning restore 1591 //XML doc } /// /// Rectangle coordinates left top right bottom. /// /// /// This type can be used with Windows API functions. The .NET Rectangle etc can't, because their fields are different. /// Has conversions from/to . /// public record struct RECT { #pragma warning disable 1591, 3008 //XML doc, CLS-compliant [JsonInclude] public int left, top; public int right, bottom; /// /// Sets all fields. /// /// /// Sets right = left + width; bottom = top + height;. To specify right/bottom instead of width/height, use instead. /// [JsonConstructor] //without it JSON deserializer sets incorrect Width/Height because does it before setting left/right public RECT(int left, int top, int width, int height) { this.left = left; this.top = top; right = left + width; bottom = top + height; } /// /// Creates with specified left, top, right and bottom. /// public static RECT FromLTRB(int left, int top, int right, int bottom) => new() { left = left, top = top, right = right, bottom = bottom }; /// /// Converts from tuple (left, top, width, height). /// public static implicit operator RECT((int L, int T, int W, int H) t) => new(t.L, t.T, t.W, t.H); public static implicit operator RECT(Rectangle r) => new(r.Left, r.Top, r.Width, r.Height); public static implicit operator Rectangle(RECT r) => new(r.left, r.top, r.Width, r.Height); public static implicit operator RectangleF(RECT r) => new(r.left, r.top, r.Width, r.Height); public static implicit operator System.Windows.Rect(RECT r) => new(r.left, r.top, r.Width, r.Height); /// /// Can round up, for example 1.7 to 2. /// public static RECT From(RectangleF r, bool round) { if (round) return new(r.Left.ToInt(), r.Top.ToInt(), r.Width.ToInt(), r.Height.ToInt()); checked { return new((int)r.Left, (int)r.Top, (int)r.Width, (int)r.Height); } } /// /// Can round up, for example 1.7 to 2. /// public static RECT From(System.Windows.Rect r, bool round) { if (round) return new(r.Left.ToInt(), r.Top.ToInt(), r.Width.ToInt(), r.Height.ToInt()); checked { return new((int)r.Left, (int)r.Top, (int)r.Width, (int)r.Height); } } //rejected. Rare. ///// ///// Sets fields like constructor . ///// //public void Set(int left, int top, int rightOrWidth, int bottomOrHeight, bool useWidthHeight = true) { // this.left = left; this.top = top; // right = rightOrWidth; bottom = bottomOrHeight; // if (useWidthHeight) { right += left; bottom += top; } //} /// /// Returns true if all fields == 0. /// public bool Is0 => left == 0 && top == 0 && right == 0 && bottom == 0; /// /// Returns true if the rectangle area is empty or invalid: right<=left || bottom<=top; /// public bool NoArea => right <= left || bottom <= top; /// /// Gets or sets width. /// public int Width { get => right - left; set { right = left + value; } } /// /// Gets or sets height. /// public int Height { get => bottom - top; set { bottom = top + value; } } /// /// Returns new POINT(left, top). /// public POINT XY => new(left, top); /// /// Returns new SIZE(Width, Height). /// public SIZE Size => new(Width, Height); /// /// Gets horizontal center. /// public int CenterX => left + (right - left) / 2; //public int CenterX => (int)(((long)left + right) / 2); /// /// Gets vertical center. /// public int CenterY => top + (bottom - top) / 2; //public int CenterY => (int)(((long)top + bottom) / 2); /// /// Returns width multiplied by height: Math.Abs((long)Width * Height). /// internal long Area_ => Math.Abs((long)Width * Height); /// /// Returns true if this rectangle contains the specified point. /// public bool Contains(int x, int y) => x >= left && x < right && y >= top && y < bottom; /// /// Returns true if this rectangle contains the specified point. /// public bool Contains(POINT p) => Contains(p.x, p.y); /// /// Returns true if this rectangle contains entire specified rectangle. /// public bool Contains(RECT r2) => r2.left >= left && r2.top >= top && r2.right <= right && r2.bottom <= bottom; /// /// Makes this rectangle bigger or smaller: left-=dx; right+=dx; top-=dy; bottom+=dy; /// Use negative dx/dy to make the rectangle smaller. Note: too big negative dx/dy can make it invalid (right < left or bottom < top). /// public void Inflate(int dx, int dy) { left -= dx; right += dx; top -= dy; bottom += dy; } /// /// Replaces this rectangle with the intersection of itself and the specified rectangle. /// If the rectangles don't intersect, makes this variable empty. /// /// true if the rectangles intersect. public bool Intersect(RECT r2) => Api.IntersectRect(out this, this, r2); /// /// Returns the intersection rectangle of two rectangles. /// If they don't intersect, returns empty rectangle. /// public static RECT Intersect(RECT r1, RECT r2) { Api.IntersectRect(out RECT r, r1, r2); return r; } /// /// Returns true if this rectangle and another rectangle intersect. /// public bool IntersectsWith(RECT r2) => Api.IntersectRect(out _, this, r2); /// /// Moves this rectangle by the specified offsets: left+=dx; right+=dx; top+=dy; bottom+=dy; /// Negative dx moves to the left. Negative dy moves up. /// public void Offset(int dx, int dy) { left += dx; right += dx; top += dy; bottom += dy; } /// /// Moves this rectangle so that left=x and right=y. Does not change and . /// public void Move(int x, int y) => Offset(x - left, y - top); /// /// Replaces this rectangle with the union of itself and the specified rectangle. /// Union is the smallest rectangle that contains two full rectangles. /// /// /// If either rectangle is empty ( or is <=0), the result is another rectangle. If both empty - empty rectangle. /// /// true if finally this rectangle is not empty. public bool Union(RECT r2) => Api.UnionRect(out this, this, r2); /// /// Returns the union of two rectangles. /// Union is the smallest rectangle that contains two full rectangles. /// /// /// If either rectangle is empty ( or is <=0), the result is another rectangle. If both empty - empty rectangle. /// public static RECT Union(RECT r1, RECT r2) { Api.UnionRect(out RECT r, r1, r2); return r; } /// /// If width or height are negative, modifies this rectangle so that they would not be negative. /// /// true - swap right/left, bottom/top; false - set right = left, bottom = top. public void Normalize(bool swap) { if (right < left) { if (swap) Math2.Swap(ref left, ref right); else right = left; } if (bottom < top) { if (swap) Math2.Swap(ref top, ref bottom); else bottom = top; } } /// /// Moves this rectangle to the specified coordinates in the specified screen, and ensures that whole rectangle is in screen. /// Final rectangle coordinates are relative to the primary screen. /// /// X coordinate in the specified screen. If default - center. Examples: 10, ^10 (reverse), .5f (fraction). /// Y coordinate in the specified screen. If default - center. /// Use this screen. If default, uses the primary screen. Example: screen.index(1). /// Use the work area, not whole screen. Default true. /// If part of rectangle is not in screen, move and/or resize it so that entire rectangle would be in screen. Default true. /// /// This function can be used to calculate new window location before creating it. If window already exists, use . /// public void MoveInScreen(Coord x, Coord y, screen screen = default, bool workArea = true, bool ensureInScreen = true) { wnd.Internal_.MoveRectInScreen(false, ref this, x, y, screen, workArea, ensureInScreen); } /// /// Moves this rectangle to the specified coordinates in another rectangle r. /// /// Another rectangle. /// X coordinate relative to r. Default - center. Examples: 10, ^10 (reverse), .5f (fraction). /// Y coordinate relative to r. Default - center. /// If part of rectangle is not in r, move and/or resize it so that entire rectangle would be in r. public void MoveInRect(RECT r, Coord x = default, Coord y = default, bool ensureInRect = false) { wnd.Internal_.MoveRectInRect(ref this, x, y, r, ensureInRect); } /// /// Adjusts this rectangle to ensure that whole rectangle is in screen. /// Initial and final rectangle coordinates are relative to the primary screen. /// /// Use this screen (see ). If default, uses screen of the rectangle (or nearest). /// Use the work area, not whole screen. Default true. /// /// This function can be used to calculate new window location before creating it. If window already exists, use . /// public void EnsureInScreen(screen screen = default, bool workArea = true) { wnd.Internal_.MoveRectInScreen(true, ref this, default, default, screen, workArea, true); } /// /// Returns true if all fields of r1 and r2 are equal or almost equal with max difference +- maxDiff. /// internal static bool EqualFuzzy_(RECT r1, RECT r2, int maxDiff) { if (Math.Abs(r1.left - r2.left) > maxDiff) return false; if (Math.Abs(r1.top - r2.top) > maxDiff) return false; if (Math.Abs(r1.right - r2.right) > maxDiff) return false; if (Math.Abs(r1.bottom - r2.bottom) > maxDiff) return false; return true; } /// /// Converts to string "{L=left T=top W=width H=height}". /// /// public override string ToString() { return $"{{L={left.ToS()} T={top.ToS()} W={Width.ToS()} H={Height.ToS()}}}"; //note: don't change the format. Some functions parse it, eg TryParse and acc in C++. //don't need R B. Rarely useful, just makes more difficult to read W H. //return $"{{L={left} T={top} R={right} B={bottom} W={Width} H={Height}}}"; } /// /// Converts to string "left top width height". /// /// public string ToStringSimple() { return $"{left.ToS()} {top.ToS()} {Width.ToS()} {Height.ToS()}"; } /// /// Formats string from main fields and properties. /// /// /// format string. Example: "({0}, {1}, {4}, {5})". /// This function passes to AppendFormat 6 values in this order: left, top, right, bottom, Width, Height. /// public string ToStringFormat(string format) { using (new StringBuilder_(out var b)) { b.AppendFormat(format, left.ToS(), top.ToS(), right.ToS(), bottom.ToS(), Width.ToS(), Height.ToS()); return b.ToString(); } } /// /// Converts string to . /// /// false if invalid string format. /// String in format "{L=left T=top W=width H=height}" () or "left top width height" (). /// public static bool TryParse(string s, out RECT r) { r = default; bool ok; if (s.Starts('{')) { ok = s.Eq(1, "L=") && s.ToInt(out r.left, 3, out int e) && s.Eq(e, " T=") && s.ToInt(out r.top, e + 3, out e) && s.Eq(e, " W=") && s.ToInt(out r.right, e + 3, out e) && s.Eq(e, " H=") && s.ToInt(out r.bottom, e + 3, out e) && s.Length == e + 1 && s[e] == '}'; //tested: regex @"^\{L=(-?\d+) T=(-?\d+) W=(-?\d+) H=(-?\d+)\}$" 9 times slower. } else { ok = s.ToInt(out r.left, 0, out int e) && s.ToInt(out r.top, e, out e) && s.ToInt(out r.right, e, out e) && s.ToInt(out r.bottom, e); } if (ok) { r.right += r.left; r.bottom += r.top; } return ok; } #pragma warning restore 1591 //XML doc } /// /// Struct with fields int start and int end. /// public record struct StartEnd { /// public int start; /// public int end; /// public StartEnd(int start, int end) { this.start = start; this.end = end; } /// /// Returns end - start. /// public int Length => end - start; /// /// Converts this to . /// Can be used to get substring, like s[x.Range] instead of s[x.start..x.end]. /// public Range Range => start..end; /// public static implicit operator Range(StartEnd s) => s.start..s.end; /// The start or end of the range is from the end. public static implicit operator StartEnd(Range r) { if (r.Start.IsFromEnd || r.End.IsFromEnd) throw new ArgumentException(); return new(r.Start.Value, r.End.Value); } /// /// Gets string span. /// [Obsolete, EditorBrowsable(EditorBrowsableState.Never)] //rarely used; easy with string.AsSpan; precedes `start` in intellisense. public RStr Span(string s) => s.AsSpan(start, end - start); /// public override string ToString() => $"({start}, {end})"; } internal unsafe struct VARIANT : IDisposable { public Api.VARENUM vt; //ushort ushort _u1; uint _u2; public nint value; public nint value2; //note: cannot use FieldOffset because of different 32/64 bit size public VARIANT(int x) { vt = Api.VARENUM.VT_I4; value = x; } public VARIANT(string x) { vt = Api.VARENUM.VT_BSTR; value = Marshal.StringToBSTR(x); } public static implicit operator VARIANT(int x) => new(x); public static implicit operator VARIANT(string x) => new(x); public int ValueInt { get { Debug.Assert(vt == Api.VARENUM.VT_I4); return (int)value; } } public BSTR ValueBstr { get { Debug.Assert(vt == Api.VARENUM.VT_BSTR); return BSTR.AttachBSTR((char*)value); } } /// /// Calls VariantClear. /// public void Dispose() { _Clear(); } void _Clear() { if (vt >= Api.VARENUM.VT_BSTR) Api.VariantClear(ref this); else vt = 0; //info: VariantClear just sets vt=0 and does not clear other members } /// /// Converts to string. /// public override string ToString() { return _ToString(); } string _ToString() { switch (vt) { case Api.VARENUM.VT_BSTR: return value == default ? null : ValueBstr.ToString(); case Api.VARENUM.VT_I4: return value.ToString(); case 0: case Api.VARENUM.VT_NULL: return null; } VARIANT v2 = default; uint lcid = 0x409; //invariant switch (vt & (Api.VARENUM)0xff) { case Api.VARENUM.VT_DATE: case Api.VARENUM.VT_DISPATCH: lcid = 0x400; break; } //LOCALE_USER_DEFAULT if (0 != Api.VariantChangeTypeEx(ref v2, this, lcid, 2, Api.VARENUM.VT_BSTR)) return null; //2 VARIANT_ALPHABOOL return v2.value == default ? null : v2.ValueBstr.ToStringAndDispose(); } /// /// Converts to string. /// Disposes this VARIANT. /// public string ToStringAndDispose() { var r = _ToString(); Dispose(); return r; } } internal unsafe struct BSTR : IDisposable { char* _p; BSTR(char* p) => _p = p; public static explicit operator BSTR(string s) => new((char*)Marshal.StringToBSTR(s)); public static explicit operator nint(BSTR b) => (nint)b._p; public static BSTR AttachBSTR(char* bstr) => new(bstr); public static BSTR CopyFrom(char* anyString) => anyString == null ? default : Api.SysAllocString(anyString); public static BSTR Alloc(int len) => Api.SysAllocStringLen(null, len); public char* Ptr => _p; /// /// Returns true if the string is null. /// public bool Is0 => _p == null; public int Length => _p == null ? 0 : Api.SysStringLen(_p); /// /// Unsafe. /// public char this[int i] => _p[i]; /// /// Converts to string. /// Does not dispose. /// public override string ToString() { var p = _p; if (p == null) return null; return Marshal.PtrToStringBSTR((IntPtr)_p); } /// /// Converts to string and disposes. /// public string ToStringAndDispose() { var p = _p; if (p == null) return null; int len = Api.SysStringLen(p); //rejected: //Some objects can return BSTR containing '\0's. Then probably the rest of string is garbage. I never noticed this but saw comments. Better allow '\0's, because in some cases it can be valid string. When invalid, it will not harm too much. //int len2 = Ptr_.Length(p, len); Debug_.PrintIf(len2 != len, "BSTR with '\\0'"); len = len2; string r = len == 0 ? "" : new string(p, 0, len); Dispose(); return r; } public void Dispose() { var t = _p; if (t != null) { _p = null; Api.SysFreeString(t); } } } } ================================================ FILE: Au/Au.Types/unused/AuClassless.cs ================================================ //rejected //namespace Au.Types //{ // /// // /// Contains functions of this library that can be called without a class, like Function() instead of Class.Function(). // /// In file global.cs: global using static Au.Types.AuClassless;. // /// Currently empty. Reserved for the future. // /// // public static class AuClassless // { // //public static void TestClassless() { } // } //} ================================================ FILE: Au/Au.cs ================================================ /*/ role classLibrary; define IDE_LA,AU,NO_GLOBAL,NO_DEFAULT_CHARSET_UNICODE; noWarnings 419,649; preBuild ..\@Au.Editor\_prePostBuild.cs; outputPath %folders.Workspace%\dll; miscFlags 1; noRef *\Au.dll; resource resources\red_cross_cursor.cur /path; /*/ //Using outputPath %folders.Workspace%\dll; instead of outputPath %folders.Workspace%\..\Au.Editor; because the compiler would replace Au.dll with the older version. // _prePostBuild will copy Au.dll to Au.Editor. ================================================ FILE: Au/Au.csproj ================================================ net10.0-windows true true false Au Au true ..\Au.snk true bin\Au.xml 419;8981 preview true true False False AnyCPU 1.1.6 LibreAutomate LibreAutomate didgeridoo LibreAutomate LibreAutomate is an automation library for Windows. Mostly desktop and web UI automation. To get the most of it, install the LibreAutomate app. Copyright (c) Gintaras Didžgalvis 2025 https://www.libreautomate.com Icon-128.png NuGet.md https://github.com/qgindi/LibreAutomate git UI automation;automate;windows;desktop;web;UI;hotkey;autotext;trigger;toolbar;keys;mouse;keyboard;clipboard;send;task MIT False portable $(DefineConstants);AU portable $(DefineConstants);AU ================================================ FILE: Au/Ext/Bitmap.Resize.cs ================================================ //The main code is from FreeImage sources. using System.Drawing; using System.Drawing.Imaging; namespace Au.Types; /// /// Used with /// public enum BRFilter { /// Produces sharper image (less blurry) than Graphics.DrawImage with InterpolationMode.HighQualityBicubic. Lanczos3, /// Produces slightly sharper image (less blurry) than Graphics.DrawImage with InterpolationMode.HighQualityBicubic. CatmullRom, /// Produces image similar to Graphics.DrawImage with InterpolationMode.HighQualityBicubic. Bicubic } public static partial class ExtMisc { /// /// Resizes this image. /// /// Resized image (new object). Returns this image if new width and height would be the same as of this image. /// /// New width. /// New height. If width or height is 0, calculates it (preserves aspect ratio). /// /// When resized, call Dispose for this object. /// /// Let the resized bitmap have PixelFormat = Format32bppPArgb. It prevents distortions at transparent-opaque boundaries. /// If false: if this bitmap has Format32bppArgb or Format32bppPArgb, does not change, else PixelFormat.Format32bppArgb. /// /// Unsupported PixelFormat. public static unsafe Bitmap Resize(this Bitmap b, int width, int height, BRFilter filter, bool dispose, bool premultiplied = false) { int wid1 = b.Width, hei1 = b.Height; if (width < 1) width = Math.Max(1, Math2.MulDiv(wid1, height, hei1)); if (height < 1) height = Math.Max(1, Math2.MulDiv(hei1, width, wid1)); if (width == wid1 && height == hei1) return b; var pf = b.PixelFormat; if (pf == PixelFormat.Format32bppPArgb) premultiplied = true; else if (premultiplied) pf = PixelFormat.Format32bppPArgb; else if(pf is not (PixelFormat.Format32bppArgb or PixelFormat.Format32bppRgb)) pf = PixelFormat.Format32bppArgb; var r = new Bitmap(width, height, pf); var d1 = b.LockBits(new(0, 0, wid1, hei1), ImageLockMode.ReadOnly, pf); var d2 = r.LockBits(new(0, 0, width, height), ImageLockMode.ReadWrite, pf); try { _FreeImage.Resize((byte*)d1.Scan0, wid1, hei1, (byte*)d2.Scan0, width, height, filter, premultiplied); } finally { b.UnlockBits(d1); r.UnlockBits(d2); } if (dispose) b.Dispose(); return r; } /// /// Scaling factor. For example 2 to make 2 times bigger, or 0.5 to make 2 times smaller. public static Bitmap Resize(this Bitmap b, double factor, BRFilter filter, bool dispose, bool premultiplied = false) { if (factor == 1) return b; var z = b.Size; int wid = (z.Width * factor).ToInt(), hei = (z.Height * factor).ToInt(); if (wid == z.Width && hei == z.Height) return b; return Resize(b, wid, hei, filter, dispose, premultiplied); } static unsafe class _FreeImage { internal static void Resize(byte* src, int srcWidth, int srcHeight, byte* dst, int dstWidth, int dstHeight, BRFilter filter, bool premultiplied) { _Filter filt = filter switch { BRFilter.Lanczos3 => new _FilterLanczos3(), BRFilter.CatmullRom => new _FilterCatmullRom(), BRFilter.Bicubic => new _FilterBicubic(), _ => null }; byte* tmp; if (dstWidth <= srcWidth) { // xy filtering if (srcWidth != dstWidth) { // source and destination widths are different so, we must // filter horizontally if (srcHeight != dstHeight) { // source and destination heights are also different so, we need // a temporary image tmp = MemoryUtil.Alloc(dstWidth * srcHeight * 4); } else { // source and destination heights are equal so, we can directly // factor into destination image (second filter method will not // be invoked) tmp = dst; } // factor source image horizontally into temporary (or destination) image _horizontalFilter(src, srcHeight, srcWidth, tmp, dstWidth, filt, premultiplied); } else { // source and destination widths are equal so, just copy the // image pointer tmp = src; } if (srcHeight != dstHeight) { // source and destination heights are different so, factor // temporary (or source) image vertically into destination image _verticalFilter(tmp, dstWidth, srcHeight, dst, dstHeight, filt, premultiplied); } } else { // yx filtering if (srcHeight != dstHeight) { // source and destination heights are different so, we must // filter vertically if (srcWidth != dstWidth) { // source and destination widths are also different so, we need // a temporary image tmp = MemoryUtil.Alloc(srcWidth * dstHeight * 4); } else { // source and destination widths are equal so, we can directly // factor into destination image (second filter method will not // be invoked) tmp = dst; } // factor source image vertically into temporary (or destination) image _verticalFilter(src, srcWidth, srcHeight, tmp, dstHeight, filt, premultiplied); } else { // source and destination heights are equal so, just copy the // image pointer tmp = src; } if (srcWidth != dstWidth) { // source and destination heights are different so, factor // temporary (or source) image horizontally into destination image _horizontalFilter(tmp, dstHeight, srcWidth, dst, dstWidth, filt, premultiplied); } } // free temporary image, if not pointing to either src or dst if (tmp != src && tmp != dst) MemoryUtil.Free(tmp); } const int FI_RGBA_RED = 2, FI_RGBA_GREEN = 1, FI_RGBA_BLUE = 0, FI_RGBA_ALPHA = 3; static void _horizontalFilter(byte* src, int height, int srcWidth, byte* dst, int dstWidth, _Filter filter, bool premultiplied) { // allocate and calculate the contributions _WeightsTable weightsTable = new(filter, dstWidth, srcWidth); // step through rows for (int y = 0; y < height; y++) { // factor each row byte* src_bits = src + srcWidth * 4 * y; byte* dst_bits = dst + dstWidth * 4 * y; for (int x = 0; x < dstWidth; x++) { // loop through row int iLeft = weightsTable.getLeftBoundary(x); int iLimit = weightsTable.getRightBoundary(x) - iLeft; byte* pixel = src_bits + iLeft * 4; double r = 0, g = 0, b = 0, a = 0; for (int i = 0; i < iLimit; i++) { // scan between boundaries // accumulate weighted effect of each neighboring pixel double weight = weightsTable.getWeight(x, i); r += (weight * pixel[FI_RGBA_RED]); g += (weight * pixel[FI_RGBA_GREEN]); b += (weight * pixel[FI_RGBA_BLUE]); a += (weight * pixel[FI_RGBA_ALPHA]); pixel += 4; } // clamp and place result in destination pixel int ai = Math.Clamp((int)(a + 0.5), 0, 0xFF); dst_bits[FI_RGBA_ALPHA] = (byte)ai; if (!premultiplied) ai = 0xFF; //else colors cannot be > alpha dst_bits[FI_RGBA_RED] = (byte)Math.Clamp((int)(r + 0.5), 0, ai); dst_bits[FI_RGBA_GREEN] = (byte)Math.Clamp((int)(g + 0.5), 0, ai); dst_bits[FI_RGBA_BLUE] = (byte)Math.Clamp((int)(b + 0.5), 0, ai); dst_bits += 4; } } } static void _verticalFilter(byte* src, int width, int srcHeight, byte* dst, int dstHeight, _Filter filter, bool premultiplied) { // allocate and calculate the contributions _WeightsTable weightsTable = new(filter, dstHeight, srcHeight); // step through columns byte* src_base = src; byte* dst_base = dst; int src_pitch = width * 4; int dst_pitch = src_pitch; for (int x = 0; x < width; x++) { // work on column x in dst int index = x * 4; byte* dst_bits = dst_base + index; // factor each column for (int y = 0; y < dstHeight; y++) { // loop through column int iLeft = weightsTable.getLeftBoundary(y); int iLimit = weightsTable.getRightBoundary(y) - iLeft; byte* src_bits = src_base + iLeft * src_pitch + index; double r = 0, g = 0, b = 0, a = 0; for (int i = 0; i < iLimit; i++) { // scan between boundaries // accumulate weighted effect of each neighboring pixel double weight = weightsTable.getWeight(y, i); r += (weight * src_bits[FI_RGBA_RED]); g += (weight * src_bits[FI_RGBA_GREEN]); b += (weight * src_bits[FI_RGBA_BLUE]); a += (weight * src_bits[FI_RGBA_ALPHA]); src_bits += src_pitch; } // clamp and place result in destination pixel int ai = Math.Clamp((int)(a + 0.5), 0, 0xFF); dst_bits[FI_RGBA_ALPHA] = (byte)ai; if (!premultiplied) ai = 0xFF; //else colors cannot be > alpha dst_bits[FI_RGBA_RED] = (byte)Math.Clamp((int)(r + 0.5), 0, ai); dst_bits[FI_RGBA_GREEN] = (byte)Math.Clamp((int)(g + 0.5), 0, ai); dst_bits[FI_RGBA_BLUE] = (byte)Math.Clamp((int)(b + 0.5), 0, ai); dst_bits += dst_pitch; } } } struct _WeightsTable { _Contribution[] _table; int _windowSize; int _lineLength; public _WeightsTable(_Filter filter, int uDstSize, int uSrcSize) { double dWidth; double dFScale; double dFilterWidth = filter.width; // factor factor double dScale = (double)uDstSize / uSrcSize; if (dScale < 1.0) { // minification dWidth = dFilterWidth / dScale; dFScale = dScale; } else { // magnification dWidth = dFilterWidth; dFScale = 1.0; } // allocate a new line contributions structure // // window size is the number of sampled pixels _windowSize = 2 * (int)Math.Ceiling(dWidth) + 1; // length of dst line (no. of rows / cols) _lineLength = uDstSize; // allocate list of contributions _table = new _Contribution[_lineLength]; for (int u = 0; u < _lineLength; u++) { // allocate contributions for every pixel _table[u].Weights = new double[_windowSize]; } // offset for discrete to continuous coordinate conversion double dOffset = 0.5 / dScale; for (int u = 0; u < _lineLength; u++) { // scan through line of contributions // inverse mapping (discrete dst 'u' to continous src 'dCenter') double dCenter = u / dScale + dOffset; // find the significant edge points that affect the pixel int iLeft = Math.Max(0, (int)(dCenter - dWidth + 0.5)); int iRight = Math.Min((int)(dCenter + dWidth + 0.5), uSrcSize); _table[u].Left = iLeft; _table[u].Right = iRight; double dTotalWeight = 0; // sum of weights (initialized to zero) for (int iSrc = iLeft; iSrc < iRight; iSrc++) { // calculate weights double weight = dFScale * filter.Filter(dFScale * (iSrc + 0.5 - dCenter)); _table[u].Weights[iSrc - iLeft] = weight; dTotalWeight += weight; } if ((dTotalWeight > 0) && (dTotalWeight != 1)) { // normalize weight of neighbouring points for (int iSrc = iLeft; iSrc < iRight; iSrc++) { // normalize point _table[u].Weights[iSrc - iLeft] /= dTotalWeight; } } // simplify the filter, discarding null weights at the right { int iTrailing = iRight - iLeft - 1; while (_table[u].Weights[iTrailing] == 0) { _table[u].Right--; iTrailing--; if (_table[u].Right == _table[u].Left) { break; } } } } // next dst pixel } [MethodImpl(MethodImplOptions.AggressiveInlining)] public double getWeight(int dst_pos, int src_pos) { return _table[dst_pos].Weights[src_pos]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public int getLeftBoundary(int dst_pos) { return _table[dst_pos].Left; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public int getRightBoundary(int dst_pos) { return _table[dst_pos].Right; } struct _Contribution { public double[] Weights; public int Left, Right; } } #if true abstract class _Filter { public abstract double width { get; } public abstract double Filter(double dVal); } class _FilterLanczos3 : _Filter { public override double width => 3; public override double Filter(double dVal) { dVal = Math.Abs(dVal); if (dVal < width) { return (_sinc(dVal) * _sinc(dVal / width)); } return 0; } static double _sinc(double value) { if (value != 0) { value *= Math.PI; return (Math.Sin(value) / value); } return 1; } } class _FilterCatmullRom : _Filter { public override double width => 2; public override double Filter(double dVal) { if (dVal < -2) return 0; if (dVal < -1) return (0.5 * (4 + dVal * (8 + dVal * (5 + dVal)))); if (dVal < 0) return (0.5 * (2 + dVal * dVal * (-5 - 3 * dVal))); if (dVal < 1) return (0.5 * (2 + dVal * dVal * (-5 + 3 * dVal))); if (dVal < 2) return (0.5 * (4 + dVal * (-8 + dVal * (5 - dVal)))); return 0; } } class _FilterBicubic : _Filter { readonly double p0, p2, p3; readonly double q0, q1, q2, q3; public _FilterBicubic() { double b = 1 / 3d, c = b; p0 = (6 - 2 * b) / 6; p2 = (-18 + 12 * b + 6 * c) / 6; p3 = (12 - 9 * b - 6 * c) / 6; q0 = (8 * b + 24 * c) / 6; q1 = (-12 * b - 48 * c) / 6; q2 = (6 * b + 30 * c) / 6; q3 = (-b - 6 * c) / 6; } public override double width => 2; public override double Filter(double dVal) { dVal = Math.Abs(dVal); if (dVal < 1) return (p0 + dVal * dVal * (p2 + dVal * p3)); if (dVal < 2) return (q0 + dVal * (q1 + dVal * (q2 + dVal * q3))); return 0; } } #else class _Filter { public readonly double width = 3; public double Filter(double dVal) { dVal = Math.Abs(dVal); if (dVal < width) { return (_sinc(dVal) * _sinc(dVal / width)); } return 0; } static double _sinc(double value) { if (value != 0) { value *= Math.PI; return (Math.Sin(value) / value); } return 1; } } #endif } } ================================================ FILE: Au/Ext/ExtMisc.cs ================================================ //note: be careful when adding functions to this class. Eg something may load winforms dlls although it seems not used. using System.Drawing; using System.Drawing.Imaging; using Microsoft.Win32; namespace Au.Types; /// /// Adds extension methods for some .NET types. /// [DebuggerStepThrough] public static unsafe partial class ExtMisc { #region value types /// /// Converts to int with rounding. /// Calls . /// /// public static int ToInt(this double t) => Convert.ToInt32(t); /// /// Converts to int with rounding. /// Calls . /// /// public static int ToInt(this float t) => Convert.ToInt32(t); /// /// Converts to int with rounding. /// Calls . /// /// public static int ToInt(this decimal t) => Convert.ToInt32(t); //rejected. Too simple, and nobody would find and use. ///// ///// Converts to int. ///// Can be used like 0xff123456.ToInt() instead of unchecked((int)0xff123456). ///// //public static int ToInt(this uint t) => unchecked((int)t); ///// ///// Converts to Color. ///// Can be used like 0xff123456.ToColor_() instead of Color.FromArgb(unchecked((int)0xff123456)). ///// ///// ///// Add 0xff000000. //internal static Color ToColor_(this uint t, bool makeOpaque = true) // => Color.FromArgb(unchecked((int)(t | (makeOpaque ? 0xff000000 : 0)))); /// /// Converts to Color. Makes opaque (alpha 0xff). /// Can be used like 0x123456.ToColor_() instead of Color.FromArgb(unchecked((int)0xff123456)). /// internal static Color ToColor_(this int t, bool bgr = false) { if (bgr) t = ColorInt.SwapRB(t); return Color.FromArgb(unchecked(0xff << 24 | t)); } /// /// Converts double to string. /// Uses invariant culture, therefore decimal point is always '.', not ',' etc. /// Calls . /// public static string ToS(this double t, string format = null) { return t.ToString(format, NumberFormatInfo.InvariantInfo); } /// /// Converts float to string. /// Uses invariant culture, therefore decimal point is always '.', not ',' etc. /// Calls . /// public static string ToS(this float t, string format = null) { return t.ToString(format, NumberFormatInfo.InvariantInfo); } /// /// Converts decimal to string. /// Uses invariant culture, therefore decimal point is always '.', not ',' etc. /// Calls . /// public static string ToS(this decimal t, string format = null) { return t.ToString(format, NumberFormatInfo.InvariantInfo); } /// /// Converts int to string. /// Uses invariant culture, therefore minus sign is always ASCII '-', not '−' etc. /// Calls . /// public static string ToS(this int t, string format = null) { return t.ToString(format, NumberFormatInfo.InvariantInfo); } /// /// Converts long to string. /// Uses invariant culture, therefore minus sign is always ASCII '-', not '−' etc. /// Calls . /// public static string ToS(this long t, string format = null) { return t.ToString(format, NumberFormatInfo.InvariantInfo); } /// /// Converts nint to string. /// Uses invariant culture, therefore minus sign is always ASCII '-', not '−' etc. /// Calls . /// public static string ToS(this nint t, string format = null) { return t.ToString(format, NumberFormatInfo.InvariantInfo); } //cref not nint.ToString because DocFX does not support it. /// /// Returns = min && this <= max) ? this : defaultValue]]>. /// internal static int EnsureValid_(this int t, int min, int max, int defaultValue = 0) => (t >= min && t <= max) ? t : defaultValue; //rare ///// ///// Returns true if t.Width <= 0 || t.Height <= 0. ///// Note: Rectangle.IsEmpty returns true only when all fields are 0. ///// //[MethodImpl(MethodImplOptions.AggressiveInlining)] //public static bool NoArea(this Rectangle t) { // return t.Width <= 0 || t.Height <= 0; //} /// /// Calls and returns start and end instead of start and length. /// /// /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static (int start, int end) GetStartEnd(this Range t, int length) { var v = t.GetOffsetAndLength(length); return (v.Offset, v.Offset + v.Length); } /// /// If this is null, returns (0, length). Else calls and returns start and end instead of start and length. /// /// /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static (int start, int end) GetStartEnd(this Range? t, int length) => t?.GetStartEnd(length) ?? (0, length); /// /// If this is null, returns (0, length). Else calls . /// /// /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static (int Offset, int Length) GetOffsetAndLength(this Range? t, int length) => t?.GetOffsetAndLength(length) ?? (0, length); /// /// Returns true if null pointer. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsNull(this ReadOnlySpan t) => t == ReadOnlySpan.Empty; //currently not used. Creates shorter string than ToString. ///// ///// Converts this Guid to Base64 string. ///// //public static string ToBase64(this Guid t) => Convert.ToBase64String(new RByte((byte*)&t, sizeof(Guid))); //rejected: too simple. We have print.it(uint), also can use $"0x{t:X}" or "0x" + t.ToString("X"). ///// ///// Converts int to hexadecimal string like "0x3A". ///// //public static string ToHex(this int t) //{ // return "0x" + t.ToString("X"); //} #endregion #region enum [MethodImpl(MethodImplOptions.AggressiveInlining)] static long _ToLong(T v) where T : unmanaged, Enum { if (sizeof(T) == 4) return *(int*)&v; if (sizeof(T) == 8) return *(long*)&v; if (sizeof(T) == 2) return *(short*)&v; return *(byte*)&v; //Compiler removes the if(sizeof(T) == n) and code that is unused with that size, because sizeof(T) is const. //Faster than with switch(sizeof(T)). It seems the switch code is considered too big to be inlined. } //same. Was faster when tested in the past. //[MethodImpl(MethodImplOptions.AggressiveInlining)] //static long _ToLong2(T v) where T : unmanaged, Enum //{ // if(sizeof(T) == 4) return Unsafe.As(ref v); // if(sizeof(T) == 8) return Unsafe.As(ref v); // if(sizeof(T) == 2) return Unsafe.As(ref v); // return Unsafe.As(ref v); //} /// /// Returns true if this enum variable has all flag bits specified in flag. /// /// /// One or more flags. /// /// The same as code (t & flag) == flag or . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Has(this T t, T flag) where T : unmanaged, Enum { #if false //Enum.HasFlag used to be slow, but now compiler for it creates the same code as with operator return t.HasFlag(flag); //However cannot use this because of JIT compiler bug: in some cases Has returns true when no flag. //Noticed it in TriggerActionThreads.Run in finally{} of actionWrapper, code o.flags.Has(TOFlags.Single). //It was elusive, difficult to debug, only in Release, and only after some time/times, when tiered JIT fully optimizes. //When Has returned true, print.it showed that flags is 0. //No bug if HasFlag called directly, not in extension method. #elif true //slightly slower than Enum.HasFlag and code as with operator var m = _ToLong(flag); return (_ToLong(t) & m) == m; #else //slower switch(sizeof(T)) { case 4: { var a = Unsafe.As(ref t); var b = Unsafe.As(ref flag); return (a & b) == b; } case 8: { var a = Unsafe.As(ref t); var b = Unsafe.As(ref flag); return (a & b) == b; } case 2: { var a = Unsafe.As(ref t); var b = Unsafe.As(ref flag); return (a & b) == b; } default: { var a = Unsafe.As(ref t); var b = Unsafe.As(ref flag); return (a & b) == b; } } //compiler removes the switch/case, because sizeof(T) is const #endif } /// /// Returns true if this enum variable has one or more flag bits specified in flags. /// /// /// One or more flags. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool HasAny(this T t, T flags) where T : unmanaged, Enum { return (_ToLong(t) & _ToLong(flags)) != 0; } //slower //[MethodImpl(MethodImplOptions.AggressiveInlining)] //public static bool HasAny5(this T t, T flags) where T : unmanaged, Enum //{ // if(sizeof(T) == 4) return (*(int*)&t & *(int*)&flags) != 0; // if(sizeof(T) == 8) return (*(long*)&t & *(long*)&flags) != 0; // if(sizeof(T) == 2) return (*(short*)&t & *(short*)&flags) != 0; // return (*(byte*)&t & *(byte*)&flags) != 0; //} /// /// Adds or removes a flag or flags. /// /// /// One or more flags to add or remove. /// If true, adds flag, else removes flag. [MethodImpl(MethodImplOptions.AggressiveOptimization)] public static void SetFlag(ref this T t, T flag, bool add) where T : unmanaged, Enum { long a = _ToLong(t), b = _ToLong(flag); if (add) a |= b; else a &= ~b; t = *(T*)&a; } /// /// Adds or removes a flag or flags. /// /// Flag(s) to add or remove. /// If true, adds the flag(s) (t |= flag), else removes (t &= ~flag). internal static void SetFlag_(this ref int t, int flag, bool add) { if (add) t |= flag; else t &= ~flag; } /// internal static void SetFlag_(this ref uint t, uint flag, bool set) { if (set) t |= flag; else t &= ~flag; } /// internal static void SetFlag_(this ref ushort t, ushort flag, bool set) { if (set) t |= flag; else t = (ushort)(t & ~flag); } /// internal static void SetFlag_(this ref byte t, byte flag, bool set) { if (set) t |= flag; else t = (byte)(t & ~flag); } #endregion #region char /// /// Returns true if character is ASCII '0' to '9'. /// public static bool IsAsciiDigit(this char c) => char.IsAsciiDigit(c); /// /// Returns true if character is ASCII 'A' to 'Z' or 'a' to 'z'. /// //public static bool IsAsciiAlpha(this char c) => (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); public static bool IsAsciiAlpha(this char c) => char.IsAsciiLetter(c); /// /// Returns true if character is ASCII 'A' to 'Z' or 'a' to 'z' or '0' to '9'. /// public static bool IsAsciiAlphaDigit(this char c) => char.IsAsciiLetterOrDigit(c); #endregion #region array /// /// Creates a copy of this array with one or more removed elements. /// /// /// /// /// /// public static T[] RemoveAt(this T[] t, int index, int count = 1) { if ((uint)index > t.Length || count < 0 || index + count > t.Length) throw new ArgumentOutOfRangeException(); int n = t.Length - count; if (n == 0) return []; var r = new T[n]; for (int i = 0; i < index; i++) r[i] = t[i]; for (int i = index; i < n; i++) r[i] = t[i + count]; return r; } /// /// Creates a copy of this array with one inserted element. /// /// /// /// Where to insert. If -1, adds to the end. /// /// public static T[] InsertAt(this T[] t, int index, T value = default) { if (index == -1) index = t.Length; else if ((uint)index > t.Length) throw new ArgumentOutOfRangeException(); var r = new T[t.Length + 1]; for (int i = 0; i < index; i++) r[i] = t[i]; for (int i = index; i < t.Length; i++) r[i + 1] = t[i]; r[index] = value; return r; } /// /// Creates a copy of this array with several inserted elements. /// /// /// /// Where to insert. If -1, adds to the end. /// /// public static T[] InsertAt(this T[] t, int index, params ReadOnlySpan values) { if (index == -1) index = t.Length; else if ((uint)index > t.Length) throw new ArgumentOutOfRangeException(); int n = values.Length; if (n == 0) return t; var r = new T[t.Length + n]; for (int i = 0; i < index; i++) r[i] = t[i]; for (int i = index; i < t.Length; i++) r[i + n] = t[i]; for (int i = 0; i < n; i++) r[i + index] = values[i]; return r; } #endregion #region IEnumerable /// /// Removes items based on a predicate. For example, all items that have certain value. /// /// /// /// /// public static void RemoveWhere(this Dictionary t, Func, bool> predicate) { foreach (var k in t.Where(predicate).Select(kv => kv.Key).ToArray()) { t.Remove(k); } } /// /// Gets a reference to a TValue in this dictionary, adding a new entry with a default value if the key does not exist. /// This extension method just calls . /// /// /// /// (); /// for (int i = 0; i < 3; i++) { /// ref var r = ref d.GetValueRefOrAddDefault("a", out bool exists); /// print.it(exists); /// if(!exists) r = 100; else r++; /// } /// print.it(d); /// ]]> /// internal static ref TValue GetValueRefOrAddDefault_(this Dictionary t, TKey key, out bool exists) { #pragma warning disable 9088 //weird and undocumented: "This returns a parameter by reference 'exists' but it is scoped to the current method" return ref CollectionsMarshal.GetValueRefOrAddDefault(t, key, out exists); } /// /// Gets a reference to a TValue in this dictionary. If the key does not exist, sets exists = false and returns a reference null. /// This extension method just calls and . /// /// Receives true if the key exists. /// internal static ref TValue GetValueRefOrNullRef_(this Dictionary t, TKey key, out bool exists) { ref TValue r = ref CollectionsMarshal.GetValueRefOrNullRef(t, key); exists = !Unsafe.IsNullRef(ref r); return ref r; } /// public static Span AsSpan(this List t) => CollectionsMarshal.AsSpan(t); /// /// Gets a reference to an item. /// List items must not be added or removed while it is in use. /// /// /// Item index. public static ref T Ref(this List t, int i) => ref CollectionsMarshal.AsSpan(t)[i]; /// /// Adds key/value to dictionary. If the key already exists, adds the value to the same key as List item and returns the List; else returns null. /// /// key/value already exists. internal static List MultiAdd_(this Dictionary t, TKey k, TValue v) where TValue : class { if (t.TryAdd(k, v)) return null; var o = t[k]; if (o is List a) { if (!a.Contains(v)) { a.Add(v); return a; } } else { var g = o as TValue; if (g == null && o != null) throw new ArgumentException("bad type"); if (v != g) { t[k] = a = new List { g, v }; return a; } } throw new ArgumentException("key/value already exists"); } /// /// If dictionary contains key k that contains value v (as single value or in List), removes the value (and key if it was single value) and returns true. /// internal static bool MultiRemove_(this Dictionary t, TKey k, TValue v) where TValue : class { if (!t.TryGetValue(k, out var o)) return false; if (o is List a) { if (!a.Remove(v)) return false; if (a.Count == 1) t[k] = a[0]; } else { var g = o as TValue; if (g == null && o != null) throw new ArgumentException("bad type"); if (v != g) return false; t.Remove(k); } return true; } /// /// If dictionary contains key k, gets its value (v) or list of values (a) and returns true. /// /// /// /// Receives single value, or null if the key has multiple values. /// Receives multiple values, or null if the key has single value. internal static bool MultiGet_(this Dictionary t, TKey k, out TValue v, out List a) where TValue : class { bool r = t.TryGetValue(k, out var o); v = o as TValue; a = o as List; if (v == null && a == null && o != null) throw new ArgumentException("bad type"); return r; } #if true /// /// Returns Length, or 0 if null. /// internal static int Lenn_(this T[] t) => t?.Length ?? 0; //internal static int Lenn_(this System.Collections.ICollection t) => t?.Count ?? 0; //slower, as well as Array /// /// Returns Count, or 0 if null. /// internal static int Lenn_(this List t) => t?.Count ?? 0; /// /// Returns true if null or Length == 0. /// internal static bool NE_(this T[] t) => (t?.Length ?? 0) == 0; /// /// Returns true if null or Count == 0. /// internal static bool NE_(this List t) => (t?.Count ?? 0) == 0; #else //still too early to use `extension` in this library. Eg then the XML file contains duplicate names (then exception in _DocumentationProvider.Create). Probably DocFX would not get it too. extension(T[] t) { //with System.Collections.ICollection slower, as well as Array /// /// Returns Length, or 0 if null. /// internal int Lenn_ => t?.Length ?? 0; /// /// Returns true if null or Length == 0. /// internal bool NE_ => (t?.Length ?? 0) == 0; } extension(List t) { /// /// Returns Count, or 0 if null. /// internal int Lenn_ => t?.Count ?? 0; /// /// Returns true if null or Count == 0. /// internal bool NE_ => (t?.Count ?? 0) == 0; } #endif /// /// Efficiently recursively gets descendants of this tree. /// /// /// /// /// internal static IEnumerable Descendants_(this IEnumerable t, Func> childSelector) { var stack = new Stack>(); var enumerator = t.GetEnumerator(); try { while (true) { if (enumerator.MoveNext()) { T element = enumerator.Current; yield return element; var e = childSelector(element)?.GetEnumerator(); if (e != null) { stack.Push(enumerator); enumerator = e; } } else if (stack.Count > 0) { enumerator.Dispose(); enumerator = stack.Pop(); } else { yield break; } } } finally { enumerator.Dispose(); while (stack.Count > 0) // Clean up in case of an exception. { enumerator = stack.Pop(); enumerator.Dispose(); } } } /// /// Efficiently recursively gets descendants of this tree. /// /// /// /// internal static System.Collections.IEnumerable Descendants_(this System.Collections.IEnumerable t, Func childSelector) { var stack = new Stack(); var enumerator = t.GetEnumerator(); while (true) { if (enumerator.MoveNext()) { object element = enumerator.Current; yield return element; var e = childSelector(element)?.GetEnumerator(); if (e != null) { stack.Push(enumerator); enumerator = e; } } else if (stack.Count > 0) { enumerator = stack.Pop(); } else { yield break; } } } #endregion #region StringBuilder /// /// Appends string as new correctly formatted sentence. /// /// this. /// /// /// Don't make the first character uppercase. /// /// If s is null or "", does nothing. /// If this is not empty, appends space. /// If s starts with a lowercase character, makes it uppercase, unless this ends with a character other than '.'. /// Appends '.' if s does not end with '.', ';', ':', ',', '!' or '?'. /// public static StringBuilder AppendSentence(this StringBuilder t, string s, bool noUcase = false) { if (!s.NE()) { bool makeUcase = !noUcase && Char.IsLower(s[0]); if (t.Length > 0) { if (makeUcase && t[^1] != '.') makeUcase = false; t.Append(' '); } if (makeUcase) { t.Append(Char.ToUpper(s[0])).Append(s, 1, s.Length - 1); } else t.Append(s); switch (s[^1]) { case '.': case ';': case ':': case ',': case '!': case '?': break; default: t.Append('.'); break; } } return t; } #endregion #region winforms /// /// Gets window handle as . /// /// A Control or Form etc. Cannot be null. /// /// Create handle if still not created. Default false (return default(wnd)). /// Unlike , creates handle even if invisible. Does not create child control handles. /// /// /// Should be called in control's thread. Calls and . /// public static wnd Hwnd(this System.Windows.Forms.Control t, bool create = false) => create || t.IsHandleCreated ? new wnd(t.Handle) : default; #endregion #region System.Drawing /// /// Draws inset or outset rectangle. /// /// /// Pen with integer width and default alignment. /// /// Draw outset. /// /// Calls with arguments corrected so that it draws inside or outside r. Does not use , it is unreliable. /// public static void DrawRectangleInset(this Graphics t, Pen pen, RECT r, bool outset = false) { if (r.NoArea) return; //pen.Alignment = PenAlignment.Inset; //no. Eg ignored if 1 pixel width. // MSDN: "A Pen that has its alignment set to Inset will yield unreliable results, sometimes drawing in the inset position and sometimes in the centered position.". var r0 = r; int w = (int)pen.Width, d = w / 2; r.left += d; r.top += d; r.right -= d = w - d; r.bottom -= d; if (outset) r.Inflate(w, w); if (!r.NoArea) { t.DrawRectangle(pen, r); } else { //DrawRectangle does not draw if width or height 0, even if pen alignment is Outset t.FillRectangle(pen.Brush, r0); //never mind dash style etc } } /// /// Draws inset rectangle of specified pen color and width. /// /// /// Creates pen and calls other overload. /// public static void DrawRectangleInset(this Graphics t, Color penColor, int penWidth, RECT r, bool outset = false) { using var pen = new Pen(penColor, penWidth); DrawRectangleInset(t, pen, r, outset); } /// /// Creates solid brush and calls . /// public static void FillRectangle(this Graphics t, Color color, RECT r) { using var brush = new SolidBrush(color); t.FillRectangle(brush, r); } /// /// Calls b.LockBits in ctor and b.UnlockBits in Dispose. /// internal struct BitmapData_ : IDisposable { Bitmap _b; BitmapData _d; public BitmapData_(Bitmap b, ImageLockMode mode, PixelFormat? pf = null) { _b = b; _d = _b.LockBits(new(default, b.Size), mode, pf ?? _b.PixelFormat); } public BitmapData_(Bitmap b, Rectangle r, ImageLockMode mode, PixelFormat? pf = null) { _b = b; _d = _b.LockBits(r, mode, pf ?? _b.PixelFormat); } public void Dispose() { _b?.UnlockBits(_d); _b = null; _d = null; } public int Width => _d.Width; public int Height => _d.Height; public int Stride => _d.Stride; public PixelFormat PixelFormat => _d.PixelFormat; public IntPtr Scan0 => _d.Scan0; } /// /// Creates a BitmapData_ object that calls b.LockBits in ctor and b.UnlockBits in Dispose. /// /// If null, uses b.PixelFormat. internal static BitmapData_ Data(this Bitmap b, ImageLockMode mode, PixelFormat? pf = null) => new BitmapData_(b, mode, pf); /// /// Creates a BitmapData_ object that calls b.LockBits in ctor and b.UnlockBits in Dispose. /// /// If null, uses b.PixelFormat. internal static BitmapData_ Data(this Bitmap b, Rectangle r, ImageLockMode mode, PixelFormat? pf = null) => new BitmapData_(b, r, mode, pf); #endregion #region other /// /// Gets a value from a subkey of this registry key. /// /// /// The name or relative path of the subkey. /// The name of the value to retrieve. /// The value to return if subkey or name does not exist. /// /// The value associated with name, or defaultValue if subkey or name not found. /// Exceptions of and . /// /// Calls and . /// public static object GetValue2(this RegistryKey t, string subkey, string name, object defaultValue = null, RegistryValueOptions options = default) { using var k = t.OpenSubKey(subkey); if (k == null) return defaultValue; return k.GetValue(name, defaultValue, options); //tested: RegGetValue same speed. } /// /// => t.GetAwaiter().GetResult(); /// internal static T Result_(this Task t) => t.GetAwaiter().GetResult(); #endregion } ================================================ FILE: Au/Ext/ExtWpf.cs ================================================ using System.Windows; using System.Windows.Controls.Primitives; using System.Windows.Interop; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Threading; using System.Windows.Data; using System.Windows.Automation; using System.Windows.Automation.Peers; using System.Windows.Automation.Provider; using System.Windows.Input; namespace Au.Types; /// /// Adds extension methods for some WPF classes. /// public static class ExtWpf { /// /// Gets native window handle of this or , or container window handle of this child object. /// /// default(wnd) if: ///
• called before creating or after closing real window; ///
• failed; ///
t is null.
/// public static wnd Hwnd(this DependencyObject t) { bool isPopup = false; switch (t) { case null: return default; case Window w: return (wnd)new WindowInteropHelper(w).Handle; //FromDependencyObject works too, but this is usually slightly faster case Popup p: t = p.Child; if (t == null) return default; isPopup = true; break; //FromVisual(Popup) returns null, FromDependencyObject too } if (PresentationSource.FromDependencyObject(t) is HwndSource hs) return (wnd)hs.Handle; if (isPopup) return default; return Hwnd(LogicalTreeHelper.GetParent(t)); } //rejected: notPopup. Not useful. ///// ///// Gets window handle of this Window, Popup or container window handle of this child object. ///// Returns default(wnd) if: called before creating real window; failed; t is null. ///// ///// ///// If this is Popup or in a Popup, get handle of popup's owner Window. //public static wnd Hwnd(this DependencyObject t, bool notPopup = false) //{ // switch(t) { // case null: return default; // case Window w: return (wnd)new WindowInteropHelper(w).Handle; //FromDependencyObject works too, but this is usually slightly faster // case Popup p when !notPopup: t = p.Child; if(t == null) return default; break; //FromVisual(Popup) returns null; or maybe owner window, not tested. // } // if(notPopup) { // var w = Window.GetWindow(t); if(w == null) return default; //if Popup or in Popup, gets owner WIndow // return (wnd)new WindowInteropHelper(w).Handle; // } // if(PresentationSource.FromDependencyObject(t) is HwndSource hs) return (wnd)hs.Handle; // return default; //} /// /// Gets IWin32Window of this window for System.Windows.Forms functions like Form.ShowDialog and ColorDialog.ShowDialog. /// public static System.Windows.Forms.IWin32Window FormOwner(this Window t) { var nw = new System.Windows.Forms.NativeWindow(); nw.AssignHandle(t.Hwnd().Handle); return nw; } /// /// Enumerates visual descendant objects, including parts of composite controls, and calls callback function f for each. /// When f returns true, stops and returns that object. Returns null if f does not return true. /// public static DependencyObject FindVisualDescendant(this DependencyObject t, Func f, bool orSelf = false) { if (orSelf && f(t)) return t; for (int i = 0, n = VisualTreeHelper.GetChildrenCount(t); i < n; i++) { var v = VisualTreeHelper.GetChild(t, i); if (f(v)) return v; v = FindVisualDescendant(v, f); if (v != null) return v; } return null; } /// /// Enumerates visual descendant objects, including parts of composite controls. /// public static IEnumerable VisualDescendants(this DependencyObject t) { for (int i = 0, n = VisualTreeHelper.GetChildrenCount(t); i < n; i++) { var v = VisualTreeHelper.GetChild(t, i); yield return v; foreach (var k in VisualDescendants(v)) yield return k; //TODO3: now creates much garbage if tree is big. // See https://stackoverflow.com/a/30441479/2547338. // See ExtMisc.Descendants. But it cannot be used here because VisualTreeHelper does not give an IEnumerable. } } /// /// Enumerates logical descendant objects, including parts of composite controls. /// public static IEnumerable LogicalDescendants(this DependencyObject t) { //foreach (var v in LogicalTreeHelper.GetChildren(t)) { // yield return v; // if (v is DependencyObject d) // foreach (var k in LogicalDescendants(d)) yield return k; //} return LogicalTreeHelper.GetChildren(t).Descendants_(o => o is DependencyObject d ? LogicalTreeHelper.GetChildren(d) : null); } /// /// Gets visual ancestors (). /// /// /// Include this object. /// Last ancestor to get. public static IEnumerable VisualAncestors(this DependencyObject t, bool andThis = false, object last = null) { for (var v = t; v != null; v = VisualTreeHelper.GetParent(v)) { if (!andThis) { andThis = true; continue; } yield return v; if (v == last) yield break; } } /// /// Calls callback function f for each visual ancestor (), and returns the ancestor for which f returns true. /// Also can return last or null. /// /// /// Include this object. /// /// When found this ancestor, stop and return last if andLast true or null if false. /// If last found, return last instead of null. /// public static DependencyObject FindVisualAncestor(this DependencyObject t, bool andThis, Func f, object last, bool andLast) { for (var v = t; v != null; v = VisualTreeHelper.GetParent(v)) { if (!andThis) { andThis = true; continue; } if (f(v)) return v; if (v == last) return andLast ? v : null; } return null; } /// /// Returns the nearest visual ancestor () of type T. /// Also can return last or null. /// /// /// Include this object. /// When found this ancestor, stop and return last if andLast true or null if false. /// If last found, return last instead of null. /// public static DependencyObject FindVisualAncestor(this DependencyObject t, bool andThis, object last, bool andLast) where T : DependencyObject { for (var v = t; v != null; v = VisualTreeHelper.GetParent(v)) { if (!andThis) { andThis = true; continue; } if (v is T r1) return r1; if (v == last) return andLast ? v : null; } return null; } /// /// Gets rectangle of this element in screen coordinates. /// /// default(RECT) if this is an invisible element (but not ) or if fails. public static RECT RectInScreen(this FrameworkElement t) { if (t is Window w) return w.Hwnd().Rect; //else would be incorrect: x/y of client area, width/height of window if (t.IsVisible) { try { Point p1 = t.PointToScreen(default), p2 = t.PointToScreen(new Point(t.ActualWidth, t.ActualHeight)); return RECT.FromLTRB(p1.X.ToInt(), p1.Y.ToInt(), p2.X.ToInt(), p2.Y.ToInt()); } catch (Exception e1) { Debug_.Print(e1); } } return default; } /// /// Sets = Hidden or Visible. /// internal static void Hide_(this UIElement t, bool hide) { t.Visibility = hide ? Visibility.Hidden : Visibility.Visible; } /// /// Sets = Collapsed or Visible. /// internal static void Collapse_(this UIElement t, bool collapse) { t.Visibility = collapse ? Visibility.Collapsed : Visibility.Visible; } /// /// Sets UI Automation name. /// public static void UiaSetName(this DependencyObject t, string name) { System.Windows.Automation.AutomationProperties.SetName(t, name); } //rejected, FBC. Looks not good. Better .IsChecked == true. /// /// Returns true if IsChecked == true. /// [EditorBrowsable(EditorBrowsableState.Never)] //[Obsolete("use code IsChecked == true")] public static bool True(this CheckBox t) => t.IsChecked == true; #if true //TODO3: does not work if this is called in ctor and caller sets Title afterwards. static unsafe void _Move(Window t, int x, int y, in RECT r, bool andSize) { var wstate = t.WindowState; if (t.IsLoaded) { var w = t.Hwnd(); if (w.Is0) throw new ObjectDisposedException("Window"); if (wstate != WindowState.Normal) t.WindowState = WindowState.Normal; if (andSize) w.MoveL(r); else w.MoveL(x, y); } else { //tested: don't need this for Popup. Its PlacementRectangle can use physical pixels. t.WindowStartupLocation = WindowStartupLocation.Manual; if (wstate == WindowState.Minimized) t.ShowActivated = false; bool maxInactive = wstate is WindowState.Maximized && !t.ShowActivated; if (maxInactive) t.WindowState = WindowState.Normal; //WPF would throw exception, although it's easy to create maximized inactive window with CreateWindowEx WindowsHook.ThreadCbt(k => { if (k.code == HookData.CbtEvent.CREATEWND) { var c = k.CreationInfo->lpcs; if (!c->style.Has(WS.CHILD)) { var name = c->Name; if (name.Length > 25 && name.StartsWith("m8KFOuCJOUmjziONcXEi3A ")) { k.hook.Dispose(); var s = name[23..].ToString(); if (name[^1] == ';') { c->x = s.ToInt(0, out int e); c->y = s.ToInt(e); } else if (RECT.TryParse(s, out var r)) { c->x = r.left; c->y = r.top; c->cx = r.Width; c->cy = r.Height; } } } } else { //didn't detect the window? Because unhooks when detects. Debug_.Print($"{k.code} {k.Hwnd}"); //Debug_.PrintIf(k.code != HookData.CbtEvent.SETFOCUS, $"{k.code} {k.Hwnd}"); //sometimes SETFOCUS before CREATEWND, and it is bad } return false; }); t.Left = double.NaN; t.Top = double.NaN; if (andSize) { t.Width = double.NaN; t.Height = double.NaN; } //temporarily change Title. I didn't find other ways to recognize the window in the hook proc. Also in title we can pass r or x y. string title = t.Title, s; if (andSize) s = "m8KFOuCJOUmjziONcXEi3A " + r.ToStringSimple(); else s = $"m8KFOuCJOUmjziONcXEi3A {x} {y};"; t.Title = s; //Need to restore Title ASAP. // In CBT hook cannot change window name in any way. // The first opportunity is WM_CREATE, it's before WPF events, but cannot set Title property there. // The sequence of .NET events depends on Window properties etc: // Default: IsVisibleChanged, HwndSource.AddSourceChangedHandler, SourceInitialized, Loaded. And several unreliable events inbetween. // SizeToContent: IsVisibleChanged, HwndSource.AddSourceChangedHandler, Loaded, SourceInitialized. // WindowInteropHelper(w).EnsureHandle(): SourceInitialized (before ShowX), IsVisibleChanged, HwndSource.AddSourceChangedHandler, Loaded. // Window without controls: Initialized, .... SourceChangedEventHandler eh = null; eh = (_, _) => { HwndSource.RemoveSourceChangedHandler(t, eh); t.Title = title; //if (wstate == WindowState.Normal && !t.ShowActivated) t.Hwnd().ZorderTop(); //it seems don't need it if (maxInactive) t.Loaded += (_, _) => _MaximizeNoActivate(t.Hwnd()); }; HwndSource.AddSourceChangedHandler(t, eh); static void _MaximizeNoActivate(wnd w) { //HACK w.GetWindowPlacement_(out var p, false); //without this, later restored rect is like max w.SetStyle(WS.MAXIMIZE, WSFlags.Add); var r = screen.of(w).WorkArea; w.SetWindowPos(SWPFlags.FRAMECHANGED | SWPFlags.NOACTIVATE | SWPFlags.NOZORDER, r.left, r.top, r.Width, r.Height); //p.showCmd = 3; w.SetWindowPlacement_(ref p, false); #if DEBUG w.GetWindowPlacement_(out var p2, false); Debug_.PrintIf(p2.rcNormalPosition != p.rcNormalPosition || p2.showCmd != 3); #endif } } } #elif true //does not change Title, but I don't like creating window handle before showing window static void _Move(Window t, int x, int y, in RECT r, bool andSize) { var wstate=t.WindowState; if(wstate!=WindowState.Normal) t.WindowState=WindowState.Normal; if(t.IsLoaded) { var w=t.Hwnd(); if(w.Is0) throw new ObjectDisposedException("Window"); if(andSize) w.MoveL(r); else w.MoveL(x, y); } else { var scrn=screen.of(new POINT(x, y)); var si=scrn.GetInfo(); var rs=si.workArea; if(andSize) { x=r.left; y=r.top; var stc=t.SizeToContent; if(stc!=SizeToContent.WidthAndHeight) { double f=96d/scrn.Dpi; if(!stc.Has(SizeToContent.Width)) t.Width=r.Width*f; if(!stc.Has(SizeToContent.Height)) t.Height=r.Height*f; } } t.WindowStartupLocation=WindowStartupLocation.Manual; t.Left=double.NaN; t.Top=double.NaN; t.Loaded+=(_,_)=> { var w=t.Hwnd(); var rw=w.Rect; x=Math.Clamp(x, rs.left, Math.Max(rs.right-rw.Width, rs.left)); y=Math.Clamp(y, rs.top, Math.Max(rs.bottom-rw.Height, rs.top)); w.MoveL(x, y); if(wstate!=WindowState.Normal) { if(wstate==WindowState.Maximized) t.SizeToContent=SizeToContent.Manual; t.WindowState=wstate; } else if(!t.ShowActivated) { w.ZorderTop(); } }; if(!si.isPrimary) { using var h=WindowsHook.ThreadCbt(d=> { if(d.code== HookData.CbtEvent.CREATEWND) unsafe { var w=d.CreationInfo(out var c, out _); if(c->style!=0 && !c->style.Has(WS.CHILD)) { //note: this does not work if ShowInTaskbar = false. Then WPF creates a "Hidden Window" before, even if owner specified. print.it(c->x, c->y, c->cx, c->cy, c->hwndParent, c->style, c->lpszClass, c->lpszName); // d.hook.Unhook(); c->x=rs.left; c->y=rs.top; //the hook receives 2 windows. At first the true window and then some other HwndWrapper* with 0 x y cx cy style parent. The second is never visibe. //We use the 'c->style!=0' to ignore it. The real window always has some styles. There is no 100% reliable and clean way to recognize the real window. //Don't unhook, because future .NET versions may create more windows, maybe some before the real window. Or in some conditions. } } return false; }); new WindowInteropHelper(t).EnsureHandle(); } } } #else //does not work well when maximized, per-monitor DPI, etc static void _Move(Window t, int x, int y, in RECT r, bool andSize) { var wstate = t.WindowState; if (wstate != WindowState.Normal) t.WindowState = WindowState.Normal; if (t.IsLoaded) { var w = t.Hwnd(); if (w.Is0) throw new ObjectDisposedException("Window"); if (andSize) w.MoveL(r); else w.MoveL(x, y); } else { //tested: don't need this for Popup. Its PlacementRectangle uses physical pixels. if (andSize) { x = r.left; y = r.top; var stc = t.SizeToContent; if (stc != SizeToContent.WidthAndHeight) { double f = 96d / screen.of(x, y).Dpi; if (!stc.Has(SizeToContent.Width)) t.Width = r.Width * f; if (!stc.Has(SizeToContent.Height)) t.Height = r.Height * f; } } t.WindowStartupLocation = WindowStartupLocation.Manual; t.Left = double.NaN; t.Top = double.NaN; //default location, somewhere near top-left of primary screen or owner's screen if (wstate == WindowState.Minimized) t.ShowActivated = false; t.SourceInitialized += (_, _) => { var w = t.Hwnd(); var v = screen.of(x, y).Info; var rs = v.workArea; if (!v.isPrimary) { using var h = WindowsHook.ThreadCbt(k => k.code == HookData.CbtEvent.ACTIVATE); //workaround for WPF bug: activates window when DPI changes w.MoveL(rs.left, rs.top); //let DPI-scale } var rw = w.Rect; x = Math.Clamp(x, rs.left, Math.Max(rs.right - rw.Width, rs.left)); y = Math.Clamp(y, rs.top, Math.Max(rs.bottom - rw.Height, rs.top)); w.MoveL(x, y); //speed: when moving to a screen with different DPI, total time is same. if (wstate != WindowState.Normal) { if (wstate == WindowState.Maximized) t.SizeToContent = SizeToContent.Manual; t.WindowState = wstate; } else if (!t.ShowActivated) { w.ZorderTop(); } }; } } #endif /// /// Sets window startup location before showing it first time. Also can move already loaded window. /// /// /// X coordinate in screen. Physical pixels. /// Y coordinate in screen. Physical pixels. /// /// The unit is physical pixels. WPF provides Left and Top properties, but the unit is logical pixels, therefore cannot set exact location on high DPI screens, especially if there are multiple screens with different DPI. /// /// If the window is already loaded, just ensures it is not maximized/minimized and calls . /// /// Else sets window location for normal state (not minimized/maximized). Temporarily changes Title. Clears WindowStartupLocation, Left, Top. Clears ShowActivated if minimized. Does not change SizeToContent. /// public static void SetXY(this Window t, int x, int y) => _Move(t, x, y, default, false); /// /// Sets window startup rectangle (location and size) before showing it first time. Also can move/resize already loaded window. /// /// /// Rectangle in screen. Physical pixels. /// /// The unit is physical pixels. WPF provides Left, Top, Width and Height properties, but the unit is logical pixels, therefore cannot set exact rectangle on high DPI screens, especially if there are multiple screens with different DPI. /// /// If the window is already loaded, just ensures it is not maximized/minimized and calls . /// /// Else sets window rectangle for normal state (not minimized/maximized). Temporarily changes Title. Clears WindowStartupLocation, Left, Top, Width, Height. Clears ShowActivated if minimized. Does not change SizeToContent. /// public static void SetRect(this Window t, RECT r) => _Move(t, 0, 0, r, true); /// /// Inserts row and adjusts row indices of children that are in other rows. /// public static void InsertRow(this Grid t, int index, RowDefinition d) { _GridShift(t, true, index, 1); t.RowDefinitions.Insert(index, d); } /// /// Inserts column and adjusts column indices of children that are in other columns. /// public static void InsertColumn(this Grid t, int index, ColumnDefinition d) { _GridShift(t, false, index, 1); t.ColumnDefinitions.Insert(index, d); } /// /// Removes row and adjusts row indices of children that are in other rows. /// /// /// /// Remove children that are in that row. public static void RemoveRow(this Grid t, int index, bool removeChildren) { if (removeChildren) _GridRemoveRowColChildren(t, true, index); t.RowDefinitions.RemoveAt(index); _GridShift(t, true, index, -1); } /// /// Removes column and adjusts column indices of children that are in other columns. /// /// /// /// Remove children that are in that column. public static void RemoveColumn(this Grid t, int index, bool removeChildren) { if (removeChildren) _GridRemoveRowColChildren(t, false, index); t.ColumnDefinitions.RemoveAt(index); _GridShift(t, false, index, -1); } /// /// Removes a child element and its row from this grid. Adjusts row indices of children that are in other rows. /// /// /// /// Also remove other elements that are in that row. public static void RemoveRow(this Grid t, UIElement e, bool removeOtherElements) { int i = Grid.GetRow(e); _GridRemoveChild(t, e); RemoveRow(t, i, removeOtherElements); } /// /// Removes a child element and its column from this grid. Adjusts column indices of children that are in other columns. /// /// /// /// Also remove other elements that are in that column. public static void RemoveColumn(this Grid t, UIElement e, bool removeOtherElements) { int i = Grid.GetColumn(e); _GridRemoveChild(t, e); RemoveColumn(t, i, removeOtherElements); } static void _GridShift(Grid g, bool rows, int startIndex, int shift) { if (startIndex >= (rows ? g.RowDefinitions.Count : g.ColumnDefinitions.Count)) return; foreach (UIElement e in g.Children) { int k = rows ? Grid.GetRow(e) : Grid.GetColumn(e); if (k < startIndex) continue; k += shift; if (rows) Grid.SetRow(e, k); else Grid.SetColumn(e, k); } } static void _GridRemoveRowColChildren(Grid g, bool row, int index) { var cc = g.Children; for (int i = cc.Count; --i >= 0;) { var e = cc[i]; int rc = row ? Grid.GetRow(e) : Grid.GetColumn(e); if (rc == index) _GridRemoveChild(g, e); } } static void _GridRemoveChild(Grid g, UIElement e) { g.Children.Remove(e); Grid.SetRow(e, 0); Grid.SetColumn(e, 0); } /// /// Adds a child element in specified row/column. /// public static void AddChild(this Grid g, UIElement e, int row, int column, int rowSpan = 1, int columnSpan = 1) { Grid.SetRow(e, row); Grid.SetColumn(e, column); if (rowSpan > 1) Grid.SetRowSpan(e, rowSpan); if (columnSpan > 1) Grid.SetColumnSpan(e, columnSpan); g.Children.Add(e); } /// /// Adds one or more columns. Like , but does not clear existing columns. /// /// public static void AddColumns(this Grid g, params WBGridLength[] widths) { foreach (var v in widths) g.ColumnDefinitions.Add(v.Column); } /// /// Adds one or more rows. Like . /// /// public static void AddRows(this Grid g, params WBGridLength[] heights) { foreach (var v in heights) g.RowDefinitions.Add(v.Row); } /// /// Gets the Text property. Returns null if it is "". /// public static string TextOrNull(this TextBox t) => t.Text.NullIfEmpty_(); /// /// Workaround for WPF bug: on DPI change tries to activate window. /// Call on WM_DPICHANED message or in OnDpiChanged override. /// public static void DpiChangedWorkaround(this Window t) => _DCW(t.Dispatcher, t.Hwnd()); /// /// Workaround for WPF bug: on DPI change tries to activate window. /// Call on WM_DPICHANED message or in OnDpiChanged override. Only if top-level window. /// public static void DpiChangedWorkaround(this HwndSource t) => _DCW(t.Dispatcher, (wnd)t.Handle); static void _DCW(Dispatcher d, wnd w) { if (wnd.active != w) { bool wasVisible = w.IsVisible; //allow to activate when opening window in non-primary screen var h = WindowsHook.ThreadCbt(k => k.code == HookData.CbtEvent.ACTIVATE && (wnd)k.wParam == w && (wasVisible || !w.IsVisible)); d.InvokeAsync(() => h.Dispose()); } } /// true if in ShowDialog, false if not, null if failed (uses reflection). internal static bool? IsModal_(this Window t) { try { var f = typeof(Window).GetField("_showingAsDialog", BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.NonPublic); return (bool)f.GetValue(t); } catch { Debug_.Print("_showingAsDialog"); return null; } } /// /// Hides the grip and/or overflow controls in this toolbar. /// Call before the toolbar is loaded. /// /// /// Hide grip. Sets SetIsLocked true. /// Hides the overflow button while it is disabled. /// Loaded. public static void HideGripAndOverflow(this ToolBar t, bool hideGrip = true, bool hideOverflow = true) { if (hideGrip) ToolBarTray.SetIsLocked(t, true); if (hideOverflow) { if (t.IsLoaded) throw new InvalidOperationException("loaded"); SizeChangedEventHandler h = null; h = (_, _) => { t.SizeChanged -= h; if (t.Template.FindName("OverflowButton", t) is ButtonBase ob) { ob.SetBinding( UIElement.VisibilityProperty, new Binding("IsEnabled") { RelativeSource = RelativeSource.Self, Converter = new BooleanToVisibilityConverter() }); } }; t.SizeChanged += h; //note: in Loaded event handler randomly does not work. Somehow t still does not have the overflow button. } } internal static wpfBuilder AddToolBar_(this wpfBuilder t, out ToolBarTray tt, out ToolBar tb, bool vertical = false, bool hideOverflow = false, bool controlBrush = false) { tt = new ToolBarTray { IsLocked = true }; if (vertical) tt.Orientation = Orientation.Vertical; tb = new ToolBar(); if (controlBrush) { tt.Background = SystemColors.ControlBrush; tb.Background = SystemColors.ControlBrush; } KeyboardNavigation.SetTabNavigation(tb, KeyboardNavigationMode.Once); tt.ToolBars.Add(tb); if (hideOverflow) tb.HideGripAndOverflow(false); t.Add(tt); return t; } /// /// Calls , which sends a request to click the button. /// Note: it's async; more info in Remarks. /// /// /// /// It is async (does not wait until finished). The button click event is raised after this function returns. /// /// Does not click if the button is disabled. /// /// Another way: button.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));. It is sync, ignores disabled state, and ignores . /// public static void UiaClick(this Button t) { //tested: does not work with CheckBox if (UIElementAutomationPeer.CreatePeerForElement(t)?.GetPattern(PatternInterface.Invoke) is IInvokeProvider ip) ip.Invoke(); } /// /// Shows the window in [preview mode](xref:code_editor). /// /// /// Called not in preview mode. /// /// Changes some window properties (owner window, location, activation, etc), terminates previous preview process, calls . If closed, calls . /// /// If called not in preview mode, calls . /// public static void Preview(this Window t) { wnd wMain = ScriptEditor.MainWindow(); if (wMain.Is0) Environment.Exit(0); if (!Environment.CommandLine.RxMatch(@" WPF_PREVIEW (-?\d+) (-?\d+)$", out var m)) Environment.Exit(0); int pid = m[1].Value.ToInt(); m[2].Value.ToInt(out long time); t.Title = "WPF preview"; t.ShowActivated = false; t.WindowStartupLocation = WindowStartupLocation.Manual; t.WindowState = WindowState.Normal; t.ShowInTaskbar = true; t.Topmost = true; t.Loaded += (_, _) => { var w = t.Hwnd(); //unsafe { int BOOL = 1; Api.DwmSetWindowAttribute(w, Api.DWMWINDOWATTRIBUTE.DWMWA_TRANSITIONS_FORCEDISABLED, &BOOL, 0); } //does not disable the inflate/deflate animation; and don't need, with it even better //move to App.Settings.wpfpreview_xy or to the right side of the primary screen if ((int)ScriptEditor.WndMsg_.Send(Api.WM_USER, 3) is int xy && xy != 0) { var p = Math2.NintToPOINT(xy); w.MoveL(p.x, p.y); w.EnsureInScreen(); } else { w.MoveInScreen(^1, .5f); } //_TerminatePrevious(); pid = 0; //async less flickering, especially when no animations, eg toolwindow t.Dispatcher.InvokeAsync(() => { _TerminatePrevious(); pid = 0; }, DispatcherPriority.ApplicationIdle); //rejected. See the commented out workaround below. Instead set Topmost = true and ShowInTaskbar = true. // The workaround may not always work, eg for other windows. // Also noticed other anomalies, eg in some cases OS activates wrong window when this window closed. //if (!WndUtil.SetOwnerWindow(w, wMain)) w.ZorderTopmost(); var hs = PresentationSource.FromVisual(t) as HwndSource; hs.AddHook(_WndProc); nint _WndProc(nint hwnd, int msg, nint wp, nint lp, ref bool handled) { var w = (wnd)hwnd; switch (msg) { //case Api.WM_NCLBUTTONDOWN or Api.WM_NCRBUTTONDOWN: //the window is already active //case Api.WM_LBUTTONDOWN or Api.WM_RBUTTONDOWN or Api.WM_MBUTTONDOWN: //for checkboxes etc // Task.Run(() => { var p = mouse.xy; Api.SetCursorPos(p.x, p.y); }); // //workaround for: if using SetOwnerWindow: // // When the window is inactive, on click nonclient (eg to close), the window hangs until mouse moved. // // Similar happens after clicking a checkbox ~30 times. Maybe other controls too. // break; case Api.WM_EXITSIZEMOVE: //save wpfpreview_xy if (!(w.IsMinimized || w.IsMaximized)) { var r = w.Rect; if (r.left == 0 && r.top == 0) r.top++; ScriptEditor.WndMsg_.Send(Api.WM_USER, 4, Math2.MakeLparam(r.left, r.top)); } break; } return 0; } }; try { t.ShowDialog(); } finally { _TerminatePrevious(); Environment.Exit(0); } void _TerminatePrevious() { if (pid == 0) return; if (process.getTimes(pid, out long tc, out _) && tc <= time) { //print.it("terminate", pid, time, tc); process.terminate(pid); } else { //print.it("bad"); //if previous task failed before calling this func, this wasn't called and therefore an even older task may be running. Close its window. foreach (var w in wnd.findAll("WPF preview", "HwndWrapper[*", "Au.Task-*.exe")) { if (!w.IsOfThisProcess) w.Close(noWait: true); } } } } } ================================================ FILE: Au/Ext/ExtXml.cs ================================================ using System.Xml.Linq; using System.Xml; namespace Au.Types { /// /// Adds extension methods for and . /// public static class ExtXml { /// /// Gets XML attribute value. /// If the attribute does not exist, returns null. /// If the attribute value is empty, returns "". /// public static string Attr(this XElement t, XName name) { return t.Attribute(name)?.Value; } /// /// Gets XML attribute value. /// If the attribute does not exist, returns defaultValue. /// If the attribute value is empty, returns "". /// public static string Attr(this XElement t, XName name, string defaultValue) { var x = t.Attribute(name); return x != null ? x.Value : defaultValue; } /// /// Gets XML attribute value. /// If the attribute does not exist, sets value = null and returns false. /// public static bool Attr(this XElement t, out string value, XName name) { value = t.Attribute(name)?.Value; return value != null; } /// /// Gets attribute value converted to int number. /// If the attribute does not exist, returns defaultValue. /// If the attribute value is empty or does not begin with a valid number, returns 0. /// public static int Attr(this XElement t, XName name, int defaultValue) { var x = t.Attribute(name); return x != null ? x.Value.ToInt() : defaultValue; } /// /// Gets attribute value converted to int number. /// If the attribute does not exist, sets value = 0 and returns false. /// If the attribute value is empty or does not begin with a valid number, sets value = 0 and returns true. /// public static bool Attr(this XElement t, out int value, XName name) { var x = t.Attribute(name); if (x == null) { value = 0; return false; } value = x.Value.ToInt(); return true; } /// /// Gets attribute value converted to long number. /// If the attribute does not exist, sets value = 0 and returns false. /// If the attribute value is empty or does not begin with a valid number, sets value = 0 and returns true. /// public static bool Attr(this XElement t, out long value, XName name) { var x = t.Attribute(name); if (x == null) { value = 0; return false; } x.Value.ToInt(out value); return true; } /// /// Gets attribute value converted to double number. /// If the attribute does not exist, sets value = 0 and returns false. /// If the attribute value is empty or is not a valid number, sets value = 0 and returns true. /// public static bool Attr(this XElement t, out double value, XName name) { var x = t.Attribute(name); if (x == null) { value = 0d; return false; } x.Value.ToNumber(out value); return true; } /// /// Gets attribute value converted to float number. /// If the attribute does not exist, sets value = 0 and returns false. /// If the attribute value is empty or is not a valid number, sets value = 0 and returns true. /// public static bool Attr(this XElement t, out float value, XName name) { var x = t.Attribute(name); if (x == null) { value = 0f; return false; } x.Value.ToNumber(out value); return true; } /// /// Gets attribute value as enum type T. /// If the attribute does not exist, sets value = default and returns false. /// If the attribute value is not a valid enum member name, sets value = default and returns true. /// public static bool Attr(this XElement t, out T value, XName name) where T : unmanaged, Enum { var x = t.Attribute(name); if (x == null) { value = default; return false; } Enum.TryParse(x.Value, out value); return true; } /// /// Returns true if this element has the specified attribute. /// public static bool HasAttr(this XElement t, XName name) { return t.Attribute(name) != null; } /// /// Gets the first found descendant element. /// /// null if not found. public static XElement Desc(this XElement t, XName name) { return t.Descendants(name).FirstOrDefault(); } /// /// Finds the first descendant element that has the specified attribute or value. /// /// null if not found. /// /// Element name. If null, can be any name. /// Attribute name. If null, uses the Value property of the element. /// Attribute value (or Value). If null, can be any value. /// Case-insensitive attributeValue. public static XElement Desc(this XElement t, XName name, XName attributeName, string attributeValue = null, bool ignoreCase = false) { foreach (var el in (name != null) ? t.Descendants(name) : t.Descendants()) { if (_CmpAttrOrValue(el, attributeName, attributeValue, ignoreCase)) return el; } return null; //speed: several times faster than XPathSelectElement } /// /// Finds all descendant elements that have the specified attribute or value. /// /// null if not found. /// /// Element name. If null, can be any name. /// Attribute name. If null, uses the Value property of the element. /// Attribute value (or Value). If null, can be any value. /// Case-insensitive attributeValue. public static IEnumerable Descs(this XElement t, XName name, XName attributeName, string attributeValue = null, bool ignoreCase = false) { foreach (var el in (name != null) ? t.Descendants(name) : t.Descendants()) { if (_CmpAttrOrValue(el, attributeName, attributeValue, ignoreCase)) yield return el; } } /// /// Gets the first found direct child element that has the specified attribute or value. /// /// null if not found. /// /// Element name. If null, can be any name. /// Attribute name. If null, uses the Value property of the element. /// Attribute value (or Value). If null, can be any value. /// Case-insensitive attributeValue. public static XElement Elem(this XElement t, XName name, XName attributeName, string attributeValue = null, bool ignoreCase = false) { foreach (var el in (name != null) ? t.Elements(name) : t.Elements()) { if (_CmpAttrOrValue(el, attributeName, attributeValue, ignoreCase)) return el; } return null; } /// /// Gets all direct child elements that have the specified attribute or value. /// /// null if not found. /// /// Element name. If null, can be any name. /// Attribute name. If null, uses the Value property of the element. /// Attribute value (or Value). If null, can be any value. /// Case-insensitive attributeValue. public static IEnumerable Elems(this XElement t, XName name, XName attributeName, string attributeValue = null, bool ignoreCase = false) { foreach (var el in (name != null) ? t.Elements(name) : t.Elements()) { if (_CmpAttrOrValue(el, attributeName, attributeValue, ignoreCase)) yield return el; } } static bool _CmpAttrOrValue(XElement el, XName attributeName, string attributeValue = null, bool ignoreCase = false) { if (attributeName != null) { var a = el.Attribute(attributeName); if (a == null) return false; if (attributeValue != null && !a.Value.Eq(attributeValue, ignoreCase)) return false; } else { if (attributeValue != null && !el.Value.Eq(attributeValue, ignoreCase)) return false; } return true; } /// /// Gets the first found direct child element. If not found, adds new empty child element. /// /// The found or added element. public static XElement ElemOrAdd(this XElement t, XName name) { var e = t.Element(name); if (e == null) t.Add(e = new XElement(name)); return e; } /// /// Gets the first found direct child element that has the specified attribute. If not found, adds new child element with the attribute. /// More info: /// /// The found or added element. public static XElement ElemOrAdd(this XElement t, XName name, XName attributeName, string attributeValue = null, bool ignoreCase = false) { var e = t.Elem(name, attributeName, attributeValue, ignoreCase); if (e == null) t.Add(e = new XElement(name, new XAttribute(attributeName, attributeValue))); return e; } /// /// Returns previous sibling element or null. /// public static XElement PrevElem(this XElement t) { for (XNode n = t.PreviousNode; n != null; n = n.PreviousNode) { if (n is XElement e) return e; } return null; } /// /// Returns next sibling element or null. /// public static XElement NextElem(this XElement t) { for (XNode n = t.NextNode; n != null; n = n.NextNode) { if (n is XElement e) return e; } return null; } /// /// Saves XML to a file in a safer way. /// Uses and . /// /// Exceptions of and . public static void SaveElem(this XElement t, string file, bool backup = false, SaveOptions? options = default) { filesystem.save(file, temp => { if (options.HasValue) t.Save(temp, options.Value); else t.Save(temp); }, backup); } /// /// Saves XML to a file in a safer way. /// Uses and /// /// Exceptions of and . public static void SaveDoc(this XDocument t, string file, bool backup = false, SaveOptions? options = default) { filesystem.save(file, temp => { if (options.HasValue) t.Save(temp, options.Value); else t.Save(temp); }, backup); } } } namespace Au.More { /// /// Loads and in a safer way. /// public static class XmlUtil { /// /// Loads XML file in a safer way. /// Uses and . /// /// /// File. Must be full path. Can contain environment variables etc, see . /// If starts with '<', loads from XML string instead. /// /// /// Not full path. /// Exceptions of . /// /// Unlike , does not replace \r\n with \n. /// public static XElement LoadElem(string file, LoadOptions options = default) => _Load(file, options, false) as XElement; /// /// Loads XML file with a namespace. /// /// Receives the default namespace. Use it to get elements, like var x2 = x1.Element(ns + "name"). /// public static XElement LoadElem(out XNamespace ns, string file, LoadOptions options = default) { var r = _Load(file, options, false) as XElement; ns = r.Name.Namespace; return r; } /// /// If XML file exists, loads it (calls ), else creates new element or returns null. /// /// /// Element name to use if file does not exist. If null, returns null if file does not exist. /// /// Not full path. /// Exceptions of . public static XElement LoadElemIfExists(string file, string elemName = null, LoadOptions options = default) { if (!filesystem.exists(file)) return elemName == null ? null : new(elemName); return _Load(file, options, false) as XElement; } /// /// Loads XML file in a safer way. /// Uses and . /// /// /// File. Must be full path. Can contain environment variables etc, see . /// If starts with '<', loads from XML string instead. /// /// /// Not full path. /// Exceptions of . /// /// Unlike , does not replace \r\n with \n. /// public static XDocument LoadDoc(string file, LoadOptions options = default) => _Load(file, options, true) as XDocument; static XContainer _Load(string file, LoadOptions options, bool doc) { if (file.Starts('<')) return _Load2(file, options, doc, true); file = pathname.NormalizeMinimally_(file); return filesystem.waitIfLocked(() => _Load2(file, options, doc, false)); static XContainer _Load2(string file, LoadOptions options, bool doc, bool isString) { using var r = isString ? new XmlTextReader(new StringReader(file)) : new XmlTextReader(file); //to preserve \r\n if (0 == (options & LoadOptions.PreserveWhitespace)) r.WhitespaceHandling = WhitespaceHandling.Significant; //to save correctly formatted. Default of XElement.Load(string). return doc ? (XContainer)XDocument.Load(r, options) : XElement.Load(r, options); } } } } ================================================ FILE: Au/Files, data/ExplorerFolder.cs ================================================ namespace Au.More; /// /// File Explorer folder window functions. /// public class ExplorerFolder { api.IWebBrowser2 _b; wnd _w, _cTab; /// /// Creates for a folder window. /// /// null if failed. /// A folder window. /// Tab name (wildcard expression). If null (default), uses the current tab. /// w invalid. public static ExplorerFolder Of(wnd w, string tab = null) { w.ThrowIfInvalid(); wnd cTab = w.Child(tab, "ShellTabWindowClass"); if (tab != null && cTab.Is0) return null; var b = _GetIWebBrowser(w, cTab); return b == null ? null : new() { _b = b, _w = w, _cTab = cTab }; } static api.IWebBrowser2 _GetIWebBrowser(wnd w, wnd cTab) { foreach (var b in _EnumShellWindows()) { try { if ((wnd)b.HWND == w) { if (!cTab.Is0 && _GetIShellBrowser(b, out var sb)) { bool ok = 0 == sb.GetWindow(out var c) && c == cTab; Marshal.ReleaseComObject(sb); if (!ok) continue; } return b; } Marshal.ReleaseComObject(b); } catch (COMException) { /*print.warning(b.LocationURL);*/ } //about:blank } return null; } static IEnumerable _EnumShellWindows() { //var sw = new _Api.ShellWindows() as _Api.IShellWindows; //4 times slower than in C++. CoCreateInstance 3 times slower. if (0 != Api.CoCreateInstance(new("9BA05972-F6A8-11CF-A442-00A0C90A8F39"), 0, 4, typeof(api.IShellWindows).GUID, out var o)) throw new AuException(); var sw = o as api.IShellWindows; for (int i = 0, n = sw.Count(); i < n; i++) { yield return sw.Item(i) as api.IWebBrowser2; } Marshal.ReleaseComObject(sw); } static bool _GetIShellBrowser(api.IWebBrowser2 b, out api.IShellBrowser sb) => Api.QueryService(b, api.SID_STopLevelBrowser, out sb); /// /// Creates for all folder windows. /// /// Skip folders that don't have a filesystem path, such as Control Panel and Recycle Bin. /// Need only tabs of this window. public static ExplorerFolder[] All(bool onlyFilesystem = false, wnd tabsOf = default) { var a = new List(); foreach (var b in _EnumShellWindows()) { try { var s = b.LocationURL; if (s.NE()) { if (onlyFilesystem) continue; } else { if (!s.Starts("file:///")) continue; //skip IE etc } wnd w = (wnd)b.HWND, c = default; if (!tabsOf.Is0 && w != tabsOf) continue; if (_GetIShellBrowser(b, out var sb)) sb.GetWindow(out c); a.Add(new() { _b = b, _w = w, _cTab = c }); } catch (COMException) { /*print.warning(b.LocationURL);*/ } //about:blank } return a.ToArray(); } /// /// Calls . /// public override string ToString() => GetFolderPath(); /// /// Gets IWebBrowser2 interface pointer. /// public object ComObject => _b; /// /// Gets window handle. /// public wnd Hwnd => _w; /// /// Gets tab control handle. /// public wnd HwndTab => _cTab; /// /// Gets folder path. /// For non-filesystem folder gets string like ":: ITEMIDLIST"; see . /// /// null if failed. public string GetFolderPath() { var s = _b.LocationURL; if (!s.NE()) { if (!s.Starts("file:///")) return null; if (0 != Api.PathCreateFromUrlAlloc(s, out var r)) return null; //note: .NET urldecoding functions produce invalid string if there are urlencoded non-ASCII characters return r; } else { //non-filesystem? if (_GetIShellBrowser(_b, out var sb)) { if (0 == sb.QueryActiveShellView(out var v1) && v1 is api.IFolderView2 f) { var p = new Pidl(f.GetFolder(typeof(api.IPersistFolder2).GUID).GetCurFolder()); return p.ToString(); } } return null; } } /// /// Gets paths of selected items. /// /// Array containing 0 or more items. /// /// For non-file-system items gets ":: ITEMIDLIST"; see . /// public string[] GetSelectedItems() { var d = _b.Document as api.ShellFolderView; var items = d?.SelectedItems(); if (items == null) return []; int n = items.Count; var a = new List(n); for (int i = 0; i < n; i++) { try { var s = items.Item(i)?.Path; if (!s.NE()) a.Add(Pidl.ClsidToItemidlist_(s)); } catch { } //once: no selection, but items.Count returned 1, and items.Item(i) returned null. Could not reproduce after select-unselect. } return a.ToArray(); } /// /// Opens a folder in this window/tab. /// /// Folder path or ":: ITEMIDLIST". Or ":back", ":forward", ":up". /// Failed. public void Open(string folder) { if (!_GetIShellBrowser(_b, out var sb)) throw new AuException(); var flag = folder switch { ":back" => api.SBSP_NAVIGATEBACK, ":forward" => api.SBSP_NAVIGATEFORWARD, ":up" => api.SBSP_PARENT, _ => 0u }; if (flag != 0) { sb.BrowseObject(0, flag | api.SBSP_SAMEBROWSER); } else { using var pidl = Pidl.FromString(folder, throwIfFailed: true); sb.BrowseObject(pidl.UnsafePtr, api.SBSP_SAMEBROWSER); } 30.ms(); while (_b.Busy != 0) 10.ms(); } /// /// Adds new tab in a folder window. /// /// A folder window. /// If not null, calls . /// for the new tab. /// Failed. /// /// To create new tab, activates the window and sends keys Ctrl+T. /// public static ExplorerFolder NewTab(wnd w, string folder = null) { _ThrowIfNoMultipleTabs(); w.Activate(); var c1 = w.Child(cn: "ShellTabWindowClass"); keys.send("Ctrl+T"); 30.ms(); wait.until(5, () => w.Child(cn: "ShellTabWindowClass") != c1); var r = ExplorerFolder.Of(w); if (folder != null) r.Open(@"C:\Test"); return r; //Alternative. // Bad: undocumented, found only in https://stackoverflow.com/a/78502949/26641797 // Good: don't need to activate window. // Same: async too. Need to wait. //c1.Send(api.WM_COMMAND, 0xA21B); } /// /// Makes this tab visible. /// /// Failed. public void SwitchToTab() { _ThrowIfNoMultipleTabs(); var s = _cTab.Name; var c = _w.ChildFast("", "Microsoft.UI.Content.DesktopChildSiteBridge"); Debug_.PrintIf(c.Is0); if (c.Is0) c = _w; var a = c.Elm["PAGETAB", s].FindAll(); if (a.Length == 0) throw new AuException(); if (a.Length == 1) { a[0].Invoke(); Debug_.PrintIf(_w.ChildFast(null, "ShellTabWindowClass") != _cTab); } else { foreach (var e in a) { e.Invoke(); if (_w.ChildFast(null, "ShellTabWindowClass") == _cTab) break; } } } static void _ThrowIfNoMultipleTabs() { if (!osVersion.minWin11_22H2) throw new InvalidOperationException("Multiple tabs not supported in this Windows version."); } //rejected: InvokeVerbOnSelection. Too limited. Verbs unknown. Many commands can be invoked with hotkeys. class api : NativeApi { [ComImport, Guid("85CB6900-4D95-11CF-960C-0080C7F4EE85")] internal interface IShellWindows { int Count(); [return: MarshalAs(UnmanagedType.IDispatch)] object Item(object index); } [ComImport, Guid("d30c1661-cdaf-11d0-8a3e-00c04fc9e26e"), InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] internal interface IWebBrowser2 { object Document { get; } //string Path { get; } //always C:\WINDOWS\ string LocationURL { get; } long HWND { get; } short Busy { get; } short Visible { get; } } [ComImport, Guid("29EC8E6C-46D3-411f-BAAA-611A6C9CAC66"), InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] internal interface ShellFolderView { FolderItems SelectedItems(); } [ComImport, Guid("744129E0-CBE5-11CE-8350-444553540000"), InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] internal interface FolderItems { int Count { get; } FolderItem Item(object index); } [ComImport, Guid("FAC32C80-CBE4-11CE-8350-444553540000"), InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] internal interface FolderItem { string Path { get; } } internal static readonly Guid SID_STopLevelBrowser = new(0x4C96BE40, 0x915C, 0x11CF, 0x99, 0xD3, 0x00, 0xAA, 0x00, 0x4A, 0xE8, 0x37); [ComImport, Guid("000214E2-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IShellBrowser { [PreserveSig] int GetWindow(out wnd phwnd); void _2(); void _3(); void _4(); void _5(); void _6(); void _7(); void _8(); void BrowseObject(nint pidl, uint wFlags); void _a(); void _b(); void _c(); [PreserveSig] int QueryActiveShellView([MarshalAs(UnmanagedType.IUnknown)] out object ppshv); } [ComImport, Guid("1AC3D9F0-175C-11d1-95BE-00609797EA4F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IPersistFolder2 { void _1(); void _2(); IntPtr GetCurFolder(); } [ComImport, Guid("1af3a467-214f-4298-908e-06b03e0b39f9"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IFolderView2 { void _1(); void _2(); IPersistFolder2 GetFolder(in Guid riid); } internal const uint SBSP_SAMEBROWSER = 0x1; internal const uint SBSP_NAVIGATEBACK = 0x4000; internal const uint SBSP_NAVIGATEFORWARD = 0x8000; internal const uint SBSP_PARENT = 0x2000; } } ================================================ FILE: Au/Files, data/FileSystemRedirection.cs ================================================ namespace Au.More; /// /// File system redirection functions. Can temporarily disable redirection, to allow this 32-bit process access the 64-bit System32 directory. /// Example: using (FileSystemRedirection r1 = new()) { r1.Disable(); ... }. /// public struct FileSystemRedirection : IDisposable { bool _redirected; IntPtr _redirValue; /// /// Calls . /// public void Dispose() { Revert(); } /// /// If , calls API Wow64DisableWow64FsRedirection, which disables file system redirection. /// /// /// The caller can call this without checking OS and process bitness. This function checks it and it is fast. /// /// Always call or , for example use finally or using statement. Not calling it is more dangerous than a memory leak. It is not called by GC. /// public void Disable() { if (osVersion.is32BitProcessAnd64BitOS) _redirected = Api.Wow64DisableWow64FsRedirection(out _redirValue); } /// /// If redirected, calls API Wow64RevertWow64FsRedirection. /// public void Revert() { if (_redirected) _redirected = !Api.Wow64RevertWow64FsRedirection(_redirValue); } /// /// Returns true if is true and path starts with . /// Most such paths are redirected, therefore you may want to disable redirection with this class. /// /// Normalized path. This function does not normalize. Also it is unaware of @"\\?\". public static bool IsSystem64PathIn32BitProcess(string path) { return 0 != _IsSystem64PathIn32BitProcess(path); } static int _IsSystem64PathIn32BitProcess(string path) { if (!osVersion.is32BitProcessAnd64BitOS) return 0; string sysDir = folders.System; if (!path.Starts(sysDir, true)) return 0; int len = sysDir.Length; if (path.Length > len && !pathname.IsSepChar_(path[len])) return 0; return len; } /// /// If is true and path starts with , replaces that path part with . /// It disables redirection to for that path. /// /// Normalized path. This function does not normalize. Also it is unaware of @"\\?\". /// Don't replace path if the file or directory exists in the redirected folder or does not exist in the non-redirected folder. public static string GetNonRedirectedSystemPath(string path, bool ifExistsOnlyThere = false) { int i = _IsSystem64PathIn32BitProcess(path); if (i == 0) return path; if (ifExistsOnlyThere && filesystem.exists(path)) return path; var s = path.ReplaceAt(0, i, folders.SystemX64); if (ifExistsOnlyThere && !filesystem.exists(s)) return path; return s; } } ================================================ FILE: Au/Files, data/FileTree.cs ================================================ namespace Au.More; /// /// Contains file infos of this and descendant folders and files retrieved by . /// Can print a formatted list of descendant sizes. /// public class FileTree : TreeBase { FEFile _f; FileTree(FEFile f) { if (f != null) { _f = f; Size = f.Size; } } /// /// Gets the info retrieved by . /// public FEFile Info => _f; /// /// Filename. /// public string Name => _f.Name; /// /// Full path. /// public string Path => _f.FullPath; /// public bool IsDirectory => _f.IsDirectory; /// /// Gets the file size. If directory, it's the sum of all descendant file sizes. /// public long Size { get; private set; } /// /// Calls and creates a tree of descendants. /// /// Folder path. /// Don't include files. /// Don't include smaller files and directories. The unit is bytes. /// If cannot access some descendant directories, ignore them and don't throw exception. Default true. /// Enumerate target directories of NTFS links, such as symbolic links and mount points. /// Called for each descendant directory. If returns false, that directory with descendants is not included. But its size contributes to ancestor sizes anyway. /// Called for each descendant file. If returns false, that file is not included. But its size contributes to ancestor sizes anyway. /// The root of the tree. You can use its descendants and . Don't use , , and . /// Exceptions of . public static FileTree Create(string path, bool onlyDirectories = false, long minSize = 0, bool ignoreInaccessible = true, bool recurseNtfsLinks = false, Func dirFilter = null, Func fileFilter = null) { var flags = ignoreInaccessible ? FEFlags.IgnoreInaccessible : 0; if (recurseNtfsLinks) flags |= FEFlags.RecurseNtfsLinks; FileTree root = new(null); _Dir(path, root, 0, false, flags); return root; void _Dir(string path, FileTree x, int level, bool skipDescendants, FEFlags flags) { foreach (var f in filesystem.enumerate(path, flags)) { f.Level = level; if (f.IsDirectory) { bool skip = skipDescendants || (dirFilter != null && !dirFilter(f)); var y = new FileTree(f); _Dir(f.FullPath, y, level + 1, skip, flags | FEFlags.UseRawPath); x.Size += y.Size; if (!skip && y.Size >= minSize) x.AddChild(y); } else { x.Size += f.Size; bool skip = skipDescendants || onlyDirectories || f.Size < minSize || (fileFilter != null && !fileFilter(f)); if (!skip) x.AddChild(new(f)); } } } } /// /// Appends to a a list of sizes and names of descendants, formatted for , without a header. /// public void PrintSizes(StringBuilder b) { _Dir(this, 0); void _Dir(FileTree x, int level) { foreach (var y in x.Children().OrderByDescending(o => o.Size)) { b.Append('\t', level); var k = y.Size / MB; b.AppendFormat(k == 0 ? "0 " : k < .001 ? "<0.001 " : k < .1 ? "{0,-7:0.###} " : k < 1 ? "{0,-7:0.##} " : k < 10 ? "{0,-7:0.#} " : "{0,-7:0.} ", k); if (y.IsDirectory) { b.AppendFormat("{1}<>", y.Path, y.Name); if (y.HasChildren) { b.AppendLine(" "); _Dir(y, level + 1); b.Length -= 2; b.Append(""); } } else { b.AppendFormat("{1}<> (file)", y.Path, y.Name); } b.AppendLine(); } } } const double MB = 1048576; /// /// Formats and prints a list of sizes and names of folder's descendant folders and optionally files, with a header. /// /// Don't include smaller files and directories. The unit is MB. /// public static void PrintSizes(string path, bool onlyDirectories = false, double minSizeMB = 0, bool ignoreInaccessible = true, bool recurseNtfsLinks = false, Func dirFilter = null, Func fileFilter = null) { var t = Create(path, onlyDirectories, (long)(minSizeMB * MB), ignoreInaccessible, recurseNtfsLinks, dirFilter, fileFilter); var b = new StringBuilder("<>Sizes (MB) of folders"); if (!onlyDirectories) b.Append(" and files"); b.Append($" in {path}<>"); if (minSizeMB > 0) b.Append($". Skipped sizes < {minSizeMB} MB."); b.AppendLine("<>"); t.PrintSizes(b); print.it(b); } } ================================================ FILE: Au/Files, data/FileWatcher.cs ================================================ namespace Au.More; /// /// Watches a file for external changes. An external change is when another process writes, creates, deletes, moves or renames the file. /// /// /// All functions are thread-safe. /// /// /// example"""); /// } /// _watcher ??= FileWatcher.Watch(_file, _OnExternalChange); /// } /// /// void _OnExternalChange() { /// _x = XElement.Load(_file); /// print.it("external change", _x); /// } /// /// public void ModifyAndSave() { /// _x.Value = Random.Shared.Next().ToString(); /// /// _watcher?.Paused = true; /// try { _x.Save(_file); } /// finally { _watcher?.Paused = false; } /// } /// } /// ]]> /// public sealed class FileWatcher : IDisposable { readonly object _dirWatcher; readonly string _file; Action _action; CancellationTokenSource _cts; long _timestamp; readonly object _lock = new(); static _Watchers s_watchers = new(); FileWatcher(object dirWatcher, string file, Action action) { _dirWatcher = dirWatcher; _file = file; _action = action; _SetTimestamp(); } /// /// Stops watching. /// /// /// Don't need to ever call this, unless you want to stop watching before the process exits. /// public void Dispose() { DisposeNoRemove_(); s_watchers.Remove(this); } internal void DisposeNoRemove_() { _action = null; lock (_lock) { _cts?.Dispose(); _cts = null; } } void _SetTimestamp() { filesystem.GetTime_(_file, out _timestamp); } /// /// When your app writes, creates, deletes, moves or renames the file, it must set this property = true during the file operation. It helps to distinguish internal and external changes. Example: . /// public bool Paused { get => field; set { if (value == field) return; field = value; if (!field && _action != null) _SetTimestamp(); } } void _Event(FileSystemEventArgs e) { //print.it(e.ChangeType, e.Name); if (Paused || _action is null) return; try { lock (_lock) { _cts?.Cancel(); _cts?.Dispose(); _cts = new(); _ = Task.Delay(100, _cts.Token).ContinueWith(t => { //to cancel previous duplicate events try { if (t.IsCanceled) return; bool exists = filesystem.GetProp_(_file, out var p); bool deleted = e.ChangeType == WatcherChangeTypes.Deleted && !exists; if (deleted) { _timestamp = 0; } else { Debug_.PrintIf(!exists); if (!exists) return; //maybe temporarily deleted; we should get 'deleted' notification afterwards Debug_.PrintIf(p.size == 0); if (p.size == 0) return; //eg VSCode saves like this: opens, clears, closes; then opens writes closes. Normally the first notification is canceled, but. if (p.time == _timestamp) return; _timestamp = p.time; } //print.it(e.ChangeType, e.Name); _action?.Invoke(); } catch (Exception ex) { Debug_.Print(ex); } }, TaskScheduler.Default); } } catch (Exception ex) { Debug_.Print(ex); } //eg _cts disposed } /// /// Starts watching a file for external changes. An external change is when another process writes, creates, deletes, moves or renames the file. /// /// Full path of a file, without environment variables. /// /// Called when detected that the file was changed externally. /// Important: when your app writes, creates, deletes, moves or renames the file, it must set = true during the file operation. /// Runs in a thread pool thread. /// /// /// A new instance used to manage the watcher. /// Don't need to dispose it, unless you want to stop watching before the process exits. /// Returns null and prints a warning if the watcher could not be created (for example the directory does not exist). /// /// /// Thrown if this method has already been called for the same file without a corresponding call to . /// If multiple notification handlers are needed, combine them into the onExternalChange delegate. /// /// public static FileWatcher Watch(string file, Action onExternalChange) => s_watchers.Add(file ?? throw new ArgumentNullException(), onExternalChange ?? throw new ArgumentNullException()); internal static void DisposeAll_() { s_watchers.DisposeAll(); } class _Watchers { class _DirectoryWatcher : FileSystemWatcher { public readonly Dictionary dFiles = new(StringComparer.OrdinalIgnoreCase); public _DirectoryWatcher(string dir) : base(dir) { } } readonly List<_DirectoryWatcher> _a = []; public FileWatcher Add(string file, Action modifiedExternally) { lock (this) { var dir = pathname.getDirectory(file); _DirectoryWatcher dw = null; foreach (var v in _a) if (v.Path.Eqi(dir)) { dw = v; break; } if (dw is null) { try { dw = new(dir) { NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite }; dw.Deleted += _Event; dw.Created += _Event; dw.Changed += _Event; dw.Renamed += _Event; dw.EnableRaisingEvents = true; _a.Add(dw); } catch (Exception ex) { //eg directory does not exist dw?.Dispose(); print.warning($"Cannot watch '{file}'. {ex}"); return null; } } FileWatcher f = new(dw, file, modifiedExternally); if (!dw.dFiles.TryAdd(pathname.getName(file), f)) throw new NotSupportedException($"A FileWatcher for '{file}' already exists"); return f; } static void _Event(object sender, FileSystemEventArgs e) { var dw = sender as _DirectoryWatcher; if (dw.dFiles.TryGetValue(e.Name, out var f)) f._Event(e); } } public void Remove(FileWatcher f) { lock (this) { var dw = f._dirWatcher as _DirectoryWatcher; dw.dFiles.Remove(pathname.getName(f._file)); if (dw.dFiles.Count == 0) { _a.Remove(dw); dw.Dispose(); } } } public void DisposeAll() { lock (this) { foreach (var dw in _a) { dw.Dispose(); foreach (var f in dw.dFiles.Values) f.DisposeNoRemove_(); dw.dFiles.Clear(); } _a.Clear(); } } public _Watchers() { //It's better to dispose/stop everything on process exit as soon as possible, to avoid invoking ModifiedExternally while/after clients execute process.thisProcessExit etc handlers. System.Runtime.Loader.AssemblyLoadContext.Default.Unloading += c => DisposeAll(); //before thisProcessExit/ProcessExit. Not on unhandled exception. AppDomain.CurrentDomain.UnhandledException += (_, _) => DisposeAll(); //never mind: maybe not the first event handler. Unlikely something bad can happen. If using JSettings, its process.thisProcessExit calls DisposeAll before saving all. } } } ================================================ FILE: Au/Files, data/Pidl.cs ================================================ namespace Au.Types; /// /// Manages an ITEMIDLIST structure that is used to identify files and other shell objects instead of a file-system path. /// /// /// Wraps an ITEMIDLIST*, also known as PIDL or LPITEMIDLIST. /// /// When calling native shell API, virtual objects can be identified only by ITEMIDLIST*. Some API also support "parsing name", which may look like "::{CLSID-1}\::{CLSID-2}". File-system objects can be identified by path as well as by ITEMIDLIST*. URLs can be identified by URL as well as by ITEMIDLIST*. /// /// The ITEMIDLIST structure is in unmanaged memory. You can dispose Pidl variables, or GC will do it later. Always dispose if creating many. /// /// This class has only ITEMIDLIST functions that are used in this library. Look for other functions on the Internet. Many of them are named with IL prefix, like ILClone, ILGetSize, ILFindLastID. /// public unsafe class Pidl : IDisposable { IntPtr _pidl; /// /// Gets the ITEMIDLIST*. /// /// /// The ITEMIDLIST memory is managed by this variable and will be freed when disposing or GC-collecting it. Use where need. /// public IntPtr UnsafePtr => _pidl; /// /// Gets the ITEMIDLIST*. /// /// /// Use to pass to API where the parameter type is . It is safer than because ensures that this variable will not be GC-collected during API call even if not referenced after the call. /// public HandleRef HandleRef => new HandleRef(this, _pidl); /// /// Returns true if the ITEMIDLIST* is null. /// public bool IsNull => _pidl == default; /// /// Assigns an ITEMIDLIST to this variable. /// /// /// ITEMIDLIST*. /// It can be created by any API that creates ITEMIDLIST. They allocate the memory with API CoTaskMemAlloc. This variable will finally free it with which calls API CoTaskMemFree. /// public Pidl(IntPtr pidl) => _pidl = pidl; /// /// Combines two ITEMIDLIST (parent and child) and assigns the result to this variable. /// /// Absolute ITEMIDLIST* (parent folder). /// Relative ITEMIDLIST* (child object). /// /// Does not free pidlAbsolute and pidlRelative. /// public Pidl(IntPtr pidlAbsolute, IntPtr pidlRelative) => _pidl = Api.ILCombine(pidlAbsolute, pidlRelative); /// /// Frees the ITEMIDLIST with and clears this variable. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// protected virtual void Dispose(bool disposing) { if (_pidl != default) { Marshal.FreeCoTaskMem(_pidl); _pidl = default; } } /// ~Pidl() { Dispose(false); } /// /// Gets the ITEMIDLIST and clears this variable so that it cannot be used and will not free the ITEMIDLIST memory. To free it use . /// public IntPtr Detach() { var R = _pidl; _pidl = default; return R; } /// /// Converts string to ITEMIDLIST and creates new variable that holds it. /// /// null if failed. /// A file-system path or URL or shell object parsing name (see ) or ":: ITEMIDLIST" (see ). Supports environment variables (see ). /// Throw exception if failed. /// Failed, and throwIfFailed is true. Probably invalid s. /// /// Calls SHParseDisplayName, except when string is ":: ITEMIDLIST". /// If ":: ITEMIDLIST", does not check whether the shell object exists. /// public static Pidl FromString(string s, bool throwIfFailed = false) { IntPtr R = FromString_(s, throwIfFailed); return (R == default) ? null : new Pidl(R); } /// /// The same as , but returns unmanaged ITEMIDLIST*. /// Later need to free it with Marshal.FreeCoTaskMem. /// /// /// If failed: true - throw ; false - return 0. internal static IntPtr FromString_(string s, bool throwIfFailed = false) { IntPtr R; s = _Normalize(s); if (s.Starts(":: ")) { var span = s.AsSpan(3); int n = span.Length / 2; R = Marshal.AllocCoTaskMem(n + 2); byte* b = (byte*)R; n = Convert2.HexDecode(span, b, n); b[n] = b[n + 1] = 0; } else { //file-system path or URL or shell object parsing name var hr = Api.SHParseDisplayName(s, default, out R, 0, null); if (hr != 0) { if (throwIfFailed) throw new AuException(hr); return default; } } return R; } /// /// The same as (CanBeUrlOrShell|DontPrefixLongPath), but ignores non-full path (returns s). /// /// File-system path or URL or "::...". static string _Normalize(string s) { s = pathname.expand(s); if (!pathname.isFullPath(s)) return s; //note: not EEV. Need to expand to ":: " etc, and EEV would not do it. return pathname.Normalize_(s, PNFlags.DontPrefixLongPath, true); } /// /// Converts the ITEMIDLIST to file path or URL or shell object parsing name or display name, depending on stringType. /// /// Returns null if this variable does not have an ITEMIDLIST (eg disposed or detached). If failed, returns null or throws exception. /// /// String format. API SIGDN. /// Often used: ///
SIGDN.NORMALDISPLAY - returns object name without path. It is best to display in UI but cannot be parsed to create ITEMIDLIST again. ///
SIGDN.FILESYSPATH - returns path if the ITEMIDLIST identifies a file system object (file or directory). Else returns null. ///
SIGDN.URL - if URL, returns URL. If file system object, returns its path like "file:///C:/a/b.txt". Else returns null. ///
SIGDN.DESKTOPABSOLUTEPARSING - returns path (if file system object) or URL (if URL) or shell object parsing name (if virtual object eg Control Panel). Note: not all returned parsing names can actually be parsed to create ITEMIDLIST again, therefore usually it's better to use instead. /// /// If failed, throw . /// Failed, and throwIfFailed is true. /// /// Calls SHGetNameFromIDList. /// public string ToShellString(SIGDN stringType, bool throwIfFailed = false) { var R = ToShellString(_pidl, stringType, throwIfFailed); GC.KeepAlive(this); return R; } /// /// Converts an ITEMIDLIST to file path or URL or shell object parsing name or display name, depending on stringType. /// /// Returns null if pidl is default(IntPtr). If failed, returns null or throws exception. /// public static string ToShellString(IntPtr pidl, SIGDN stringType, bool throwIfFailed = false) { if (pidl == default) return null; var hr = Api.SHGetNameFromIDList(pidl, stringType, out string R); if (hr == 0) return R; if (throwIfFailed) throw new AuException(hr); return null; } /// /// Converts the ITEMIDLIST to string. /// If it identifies an existing file-system object (file or directory), returns path. If URL, returns URL. Else returns ":: ITEMIDLIST" (see ). /// /// null if this variable does not have an ITEMIDLIST (eg disposed or detached). public override string ToString() { var R = ToString(_pidl); GC.KeepAlive(this); return R; } #if true /// /// This overload uses an ITEMIDLIST* that is not stored in a variable. /// public static string ToString(IntPtr pidl) { if (pidl == default) return null; Api.IShellItem si = null; try { if (0 == Api.SHCreateShellItem(default, null, pidl, out si)) { //if(0 == Api.SHCreateItemFromIDList(pidl, Api.IID_IShellItem, out si)) { //same speed //if(si.GetAttributes(0xffffffff, out uint attr)>=0) print.it(attr); if (si.GetAttributes(Api.SFGAO_BROWSABLE | Api.SFGAO_FILESYSTEM, out uint attr) >= 0 && attr != 0) { var f = (0 != (attr & Api.SFGAO_FILESYSTEM)) ? SIGDN.FILESYSPATH : SIGDN.URL; if (0 == si.GetDisplayName(f, out var R)) return R; } } } finally { Api.ReleaseComObject(si); } return ToHexString(pidl); } //this version is 40% slower with non-virtual objects (why?), but with virtual objects same speed as SIGDN_DESKTOPABSOLUTEPARSING. //The fastest (update: actually not) version would be to call ToShellString_(SIGDN_DESKTOPABSOLUTEPARSING), and then call ToHexString if it returns not a path or URL. But it is unreliable, because can return string in any format, eg "Microsoft.WindowsCalculator_8wekyb3d8bbwe!App". #elif false //this version works, but with virtual objects 2 times slower than SIGDN_DESKTOPABSOLUTEPARSING (which already is very slow with virtual). public static string ToString(IntPtr pidl) { if(pidl == default) return null; var R = ToShellString(pidl, SIGDN.FILESYSPATH); if(R == null) R = ToShellString(pidl, SIGDN.URL); if(R == null) R = ToHexString(pidl); return R; } #elif true //this version works, but with virtual objects 30% slower. Also 30% slower for non-virtual objects (why?). public static string ToString(IntPtr pidl) { if(pidl == default) return null; Api.IShellItem si = null; try { if(0 == Api.SHCreateShellItem(default, null, pidl, out si)) { string R = null; if(0 == si.GetDisplayName(SIGDN.FILESYSPATH, out R)) return R; if(0 == si.GetDisplayName(SIGDN.URL, out R)) return R; } } finally { Api.ReleaseComObject(si); } return ToHexString(pidl); } #else //SHGetPathFromIDList also slow. //SHBindToObject cannot get ishellitem. #endif /// /// Returns string ":: ITEMIDLIST". /// Returns null if this variable does not have an ITEMIDLIST (eg disposed or detached). /// /// /// The string can be used with some functions of this library, mostly of classes , and . Cannot be used with native and .NET functions. /// public string ToHexString() { var R = ToHexString(_pidl); GC.KeepAlive(this); return R; } /// /// Returns string ":: ITEMIDLIST". /// This overload uses an ITEMIDLIST* that is not stored in a variable. /// public static string ToHexString(IntPtr pidl) { if (pidl == default) return null; int n = Api.ILGetSize(pidl) - 2; //API gets size with the terminating '\0' (2 bytes) if (n < 0) return null; if (n == 0) return ":: "; //shell root - Desktop return ":: " + Convert2.HexEncode((void*)pidl, n); } //rejected: use base64 ITEMIDLIST. Shorter, but cannot easily split, for example in folders.UnexpandPath. /// /// If s starts with "::{", converts to ":: ITEMIDLIST". Else returns s. /// internal static string ClsidToItemidlist_(string s) { if (s != null && s.Starts("::{")) { using var pidl = FromString(s); if (pidl != null) return pidl.ToString(); } return s; } /// /// Returns true if ITEMIDLIST values are equal. /// public bool ValueEquals(IntPtr pidl) => Api.ILIsEqual(_pidl, pidl); } ================================================ FILE: Au/Files, data/TempFile.cs ================================================ namespace Au.More; /// /// Creates unique name for a temporary file and later auto-deletes the file. /// /// /// Use code like in the example to auto-delete the temporary file if exists. Or call . Else this class does not delete the file. /// /// /// /// public sealed class TempFile : IDisposable { readonly string _file; /// /// Creates full path string with a unique filename (GUID) for a temporary file. /// /// Filename extension with dot. Default: ".tmp". Can be null. /// Parent directory. If null (default), uses . The function creates the directory if does not exist. /// directory not full path. /// Failed to create directory. public TempFile(string ext = ".tmp", string directory = null) { filesystem.createDirectory(directory ??= folders.ThisAppTemp); _file = pathname.combine(directory, Guid.NewGuid().ToString()) + ext; } /// /// Deletes the file if exists. /// /// /// Does not throw exception if fails to delete. /// public void Dispose() { filesystem.delete(_file, FDFlags.CanFail); } /// /// Gets the file path. /// public string File => _file; /// /// Returns the file path. /// public static implicit operator string(TempFile f) => f._file; /// /// Returns the file path. /// public override string ToString() => _file; } ================================================ FILE: Au/Files, data/filesystem-types.cs ================================================ namespace Au.Types; /// /// File system entry type - file, directory, NTFS link, whether it exists and is accessible. /// The enum value NotFound is 0; AccessDenied is negative ((int)0x80000000); other values are greater than 0. /// internal enum FileIs_ { /// Does not exist. NotFound = 0, /// Is file, and not NTFS link. File = 1, /// Is directory, and not NTFS link. Directory = 2, /// Is NTFS link to file. NtfsLinkFile = 5, /// Is NTFS link to directory. NtfsLinkDirectory = 6, /// Exists but this process cannot access it and get attributes. AccessDenied = int.MinValue, } /// /// Contains file or directory attributes. Tells whether it exists, is directory, readonly, hidden, system, NTFS link. /// See . /// public struct FAttr { readonly FileAttributes _a; readonly bool _exists, _unknown, _ntfsLink; /// Attributes, or 0 if does not exist or can't get attributes. /// True if exists and can get attributes. False if does not exist. null if exists but can't get attributes. /// Is a NTFS link, such as symbolic link or mount point. internal FAttr(FileAttributes attributes, bool? exists, bool ntfsLink) { _a = attributes; _exists = exists == true; _unknown = exists == null; _ntfsLink = ntfsLink; } /// /// Returns file or directory attributes. Returns 0 if false. /// public FileAttributes Attributes => _a; /// /// Returns . /// public static implicit operator bool(FAttr fa) => fa.Exists; /// /// Returns 0 if !, 1 if , 2 if . Can be used with switch. /// public static implicit operator int(FAttr fa) => !fa.Exists ? 0 : (fa.Directory ? 2 : 1); /// /// Exists and is accessible ( false). /// See also , . /// public bool Exists => _exists; /// /// Exists but this process cannot access it and get attributes (error "access denied"). Then other bool properties return false. /// public bool Unknown => _unknown; /// /// Is file (not directory), or NTFS link to a file (if true). /// public bool File => 0 == (_a & FileAttributes.Directory) && _exists; /// /// Is directory, or NTFS link to a directory (if true). /// public bool Directory => 0 != (_a & FileAttributes.Directory); /// /// It is a NTFS link, such as symbolic link, junction or mount point. Don't confuse with shell links (shortcuts). /// If true, the target is a file. If true, the target is a directory. /// public bool IsNtfsLink => _ntfsLink; /// /// Has . /// public bool IsReadonly => 0 != (_a & FileAttributes.ReadOnly); /// /// Has . /// public bool IsHidden => 0 != (_a & FileAttributes.Hidden); /// /// Has . /// public bool IsSystem => 0 != (_a & FileAttributes.System); /// public override string ToString() { return Unknown ? "unknown" : (Exists ? $"{{ Directory={Directory}, IsNtfsLink={IsNtfsLink}, Attributes={Attributes} }}" : "doesn't exist"); } } /// /// Flags for and some other functions. /// [Flags] public enum FAFlags { ///Pass path to the API as it is, without any normalizing and validating. UseRawPath = 1, /// ///If failed, return false and don't throw exception. ///Then, if you need error info, you can use . If the file/directory does not exist, it will return ERROR_FILE_NOT_FOUND or ERROR_PATH_NOT_FOUND or ERROR_NOT_READY. ///If failed and the native error code is ERROR_ACCESS_DENIED or ERROR_SHARING_VIOLATION, the returned attributes will be (FileAttributes)(-1). The file probably exists but is protected so that this process cannot access and use it. Else attributes will be 0. /// DontThrow = 2, } /// /// File or directory properties. Used with . /// public record struct FileProperties { /// public FileAttributes Attributes { get; set; } ///File size. For directories it is usually 0. public long Size { get; set; } /// public DateTime LastWriteTimeUtc { get; set; } /// public DateTime CreationTimeUtc { get; set; } ///Note: this is unreliable. The operating system may not record this time automatically. public DateTime LastAccessTimeUtc { get; set; } /// /// It is a NTFS link, such as symbolic link or mount point. Don't confuse with shell links (shortcuts). /// public bool IsNtfsLink { get; set; } } /// /// flags for . /// [Flags] public enum FEFlags { /// /// Enumerate all descendants, not only direct children. Also known as "recurse subdirectories". /// AllDescendants = 1, /// /// Also enumerate target directories of NTFS links, such as symbolic links and mount points. Use with AllDescendants. /// RecurseNtfsLinks = 2, /// /// Skip files and subdirectories that have Hidden attribute. /// SkipHidden = 4, /// /// Skip files and subdirectories that have Hidden and System attributes (both). /// These files/directories usually are created and used only by the operating system. Drives usually have several such directories. Another example - thumbnail cache files. /// Without this flag the function skips only these hidden-system root directories when enumerating a drive: $Recycle.Bin, System Volume Information, Recovery. If you want to include them too, use network path of the drive, for example @"\\localhost\D$\" for D drive. /// SkipHiddenSystem = 8, //note: must match FCFlags /// /// If fails to get the contents of the directory or a subdirectory because of its security settings, assume that the [sub]directory is empty. /// Without this flag then throws exception or calls errorHandler. /// IgnoreInaccessible = 0x10, //note: must match FCFlags /// /// Get only files and not subdirectories. /// Note: the dirFilter callback function is called just to ask whether to include children. /// OnlyFiles = 0x20, /// /// Don't call and don't throw exception for non-full path. /// UseRawPath = 0x40, /// /// Let be path relative to the specified directory path. Like @"\name.txt" or @"\subdirectory\name.txt" instead of "name.txt". /// NeedRelativePaths = 0x80, //rejected. Rarely used. Can use FileSystemRedirection, it's public. ///// ///// Temporarily disable file system redirection in this thread of this 32-bit process running on 64-bit Windows. ///// Then you can enumerate the 64-bit System32 folder in your 32-bit process. ///// Uses API Wow64DisableWow64FsRedirection. ///// For vice versa (in 64-bit process enumerate the 32-bit System folder), instead use path folders.SystemX86. ///// //DisableRedirection = 0x100, } /// /// flags for and some other similar functions. /// Used only when copying directory. /// [Flags] public enum FCFlags { //note: these values must match the corresponding FEFlags values. /// /// Skip descendant files and directories that have Hidden and System attributes (both). /// They usually are created and used only by the operating system. Drives usually have several such directories. Another example - thumbnail cache files. /// They often are protected and would fail to copy, ruining whole copy operation. /// Without this flag the function skips only these hidden-system root directories when enumerating a drive: $Recycle.Bin, System Volume Information, Recovery. /// SkipHiddenSystem = 8, /// /// If fails to get the contents of the directory or a subdirectory because of its security settings, don't throw exception but assume that the [sub]directory is empty. /// IgnoreInaccessible = 0x10, /// /// Don't create subdirectories that after applying all filters would be empty. /// NoEmptyDirectories = 0x10000, } /// /// flags for . /// [Flags] public enum FDFlags { /// /// Send to the Recycle Bin. If not possible, delete anyway, unless used CanFail. /// Why could be not possible: 1. The file is in a removable drive (most removables don't have a recycle bin). 2. The file is too large. 3. The path is too long. 4. The Recycle Bin is not used on that drive (it can be set in the Recycle Bin Properties dialog). 5. This process is non-UI-interactive, eg a service. 6. Unknown reasons. /// Note: it is much slower. To delete multiple, use . /// RecycleBin = 1, /// /// If fails to delete, don't wait/retry and don't throw exception. /// CanFail = 2, //rejected. Rarely useful. Maybe in the future. ///// ///// Fail if has read-only attribute. ///// //ReadonlyFail = 4, } /// /// Contains name and other main properties of a file or subdirectory retrieved by . /// The values are not changed after creating the variable. /// public class FEFile { internal FEFile(string name, string fullPath, in Api.WIN32_FIND_DATA d, int level) { Name = name; FullPath = fullPath; Attributes = d.dwFileAttributes; Size = (long)d.nFileSizeHigh << 32 | d.nFileSizeLow; LastWriteTimeUtc = DateTime.FromFileTimeUtc(d.ftLastWriteTime); //fast, sizeof 8 CreationTimeUtc = DateTime.FromFileTimeUtc(d.ftCreationTime); _level = (short)level; ReparseTag = d.dwReserved0; } /// /// Gets file name. Or relative path if used . /// public string Name { get; } /// /// Gets full path. /// public string FullPath { get; } /// /// Gets filename extension. Returns "" if directory. /// public string Extension => IsDirectory ? "" : pathname.getExtension(Name); //note: if null for directory, then OrderBy throws exception /// /// Returns file size. For directories it is usually 0. /// public long Size { get; } /// public DateTime LastWriteTimeUtc { get; } /// public DateTime CreationTimeUtc { get; } /// public FileAttributes Attributes { get; } /// /// It is a directory. Or a NTFS link to a directory (see ). /// public bool IsDirectory { get { return (Attributes & FileAttributes.Directory) != 0; } } /// /// Descendant level. /// 0 if direct child of the directory (directoryPath), 1 if child of child, and so on. /// public int Level { get => _level; internal set { _level = (short)value; } } short _level; /// /// It is a NTFS link, such as symbolic link or mount point. Don't confuse with shell links (shortcuts). /// public bool IsNtfsLink => Attributes.Has(FileAttributes.ReparsePoint) && 0 != (ReparseTag & 0x20000000); /// /// WIN32_FIND_DATA.dwReserved0. /// public uint ReparseTag { get; } /// /// Returns . /// public override string ToString() => FullPath; //This could be more dangerous than useful. ///// ///// Returns FullPath. ///// //public static implicit operator string(FEFile f) { return f?.FullPath; } } /// /// What to do if the destination directory contains a file or directory with the same name as the source file or directory when copying, moving or renaming. /// /// /// Used with , and similar functions. /// When renaming or moving, if the destination is the same as the source, these options are ignored and the destination is simply renamed. For example when renaming "file.txt" to "FILE.TXT". /// public enum FIfExists { /// Throw exception. Default. Fail, /// Delete destination. Delete, /// Rename (backup) destination. RenameExisting, /// /// If destination directory exists, merge the source directory into it, replacing existing files. /// If destination file exists, deletes it. /// If destination directory exists and source is file, fails. /// MergeDirectory, /// Copy/move with a different name. RenameNew, #if not_implemented /// Display a dialog asking the user what to do. Ask, #endif } /// /// Used with /// public enum FPFormat { /// /// With long-path prefix ("\\?\" or "\\?\UNC\") if path length > . This is default. /// PrefixIfLong, /// /// Always with long-path prefix ("\\?\" or "\\?\UNC\"). /// PrefixAlways, /// /// Without long-path prefix, even if the path is very long. /// PrefixNever, /// /// With volume GUID (API GetFinalPathNameByHandle flag VOLUME_NAME_GUID). /// If it fails (eg network path), gets path with prefix, like PrefixAlways. /// VolumeGuid } /// /// See . /// public enum CPResult { /// /// The paths are unrelated. /// Example: pathA: @"C:\Dir1\File1.txt", pathB: @"C:\Dir2\File1.txt". /// None, /// /// Both paths are of the same file or directory. /// Example: pathA: @"C:\Dir1\File1.txt", pathB: @"C:\Dir2\..\Dir1\File1.txt". /// Same, /// /// pathA is of a directory that contains file or directory specified by pathB. /// Example: pathA: @"C:\Dir1", pathB: @"C:\Dir1\Dir2\File1.txt". /// AContainsB, /// /// pathB is of a directory that contains file or directory specified by pathA. /// Example: pathA: @"C:\Dir1\Dir2\File1.txt", pathB: @"C:\Dir1". /// BContainsA, /// /// Failed. Probably one (or both) of specified files does not exist. /// The function supports . /// Failed, } /// /// See . /// public record struct FileId(int VolumeSerialNumber, long FileIndex); /// /// See /// public enum CSLink { /// Symbolic link to file. File, /// Symbolic link to directory. Directory, /// /// Junction to directory. /// /// Usually junctions work like symbolic links, but there are differences when creating them: ///
• Don't need admin privileges to create. ///
• Target must be full path. Fails if relative path. ///
• Target must be on this computer. Fails if on a network computer. ///
• Target must be directory. Fails if file. /// /// Some programs interpret junctions differently. For example git adds the target directory. ///
Junction, /// /// Junction to local directory or symbolic link to network directory. /// JunctionOrSymlink, } ================================================ FILE: Au/Files, data/filesystem.cs ================================================ //#define TEST_FINDFIRSTFILEEX using Microsoft.Win32; namespace Au; /// /// File and directory functions. Copy, move, delete, find, get properties, enumerate, create directory, load/save, etc. /// /// /// Also you can use .NET file system classes, such as and in System.IO namespace. In the past they were too limited and unsafe to use, for example no long paths, too many exceptions, difficult to recursively enumerate directories containing protected items. Later improved, but this class still has something they don't, for example environment variables in path, safe load/save. This class does not have low-level functions to open/read/write files. /// /// Most functions support only full path. Most of them throw if passed a filename or relative path, ie in "current directory". Using current directory is unsafe. /// Most functions support extended-length paths (longer than 259). Such local paths should have @"\\?\" prefix, like @"\\?\C:\...". Such network path should be like @"\\?\UNC\server\share\...". See , . Many functions support long paths even without prefix. /// /// Disk drives like @"C:\" or "C:" are directories too. /// public static partial class filesystem { #region exists, attributes, properties /// /// Contains the write/create times, size and attributes of a file or directory. /// The equality method and operators compare only , and attributes Directory and ReparsePoint; not . /// /// The last write time UTC as FILETIME. /// 0 if directory. /// /// The creation time UTC as FILETIME. internal record struct Prop_(long time, long size, FileAttributes attr, long timeCreated) { public bool Equals(Prop_ p) => p.time == time && p.size == size && (p.attr & (FileAttributes.Directory | FileAttributes.ReparsePoint)) == (attr & (FileAttributes.Directory | FileAttributes.ReparsePoint)); public override int GetHashCode() => time.GetHashCode(); public DateTime TimeAsDateTime => DateTime.FromFileTimeUtc(time); } /// /// Gets the last write time, size and attributes of a file or directory. /// /// Full path. The function just asserts full path and calls . /// /// false if does not exist or access denied. No exceptions. internal static unsafe bool GetProp_(string path, out Prop_ prop) { Debug.Assert(pathname.isFullPath(path)); prop = default; path = pathname.prefixLongPathIfNeed(path); if (!Api.GetFileAttributesEx(path, 0, out var d)) { if (!_GetAttributesOnError(path, FAFlags.DontThrow, out _, out _, &d)) return false; } prop = new(d.ftLastWriteTime, d.Size, d.dwFileAttributes, d.ftCreationTime); return true; } /// /// Gets the last write time of a file or directory. /// /// Full path. The function just asserts full path and calls . /// Time UTC as FILETIME. /// false if does not exist or access denied. No exceptions. internal static bool GetTime_(string path, out long time) { bool ok = GetProp_(path, out var p); time = p.time; return ok; } /// /// Gets file or directory attributes, size and times. /// /// false if the file/directory does not exist. /// Full path. Supports @"\.." etc. If flag UseRawPath not used, supports environment variables (see ). /// Receives properties. /// /// Not full path (when not used flag UseRawPath). /// The file/directory exists but failed to get its properties. Not thrown if used flag DontThrow. /// /// Calls API GetFileAttributesEx. Supports (useful with flag DontThrow). /// For NTFS links, gets properties of the link, not of its target. /// You can also get most of these properties with . /// public static unsafe bool getProperties(string path, out FileProperties properties, FAFlags flags = 0) { properties = new(); if (0 == (flags & FAFlags.UseRawPath)) path = pathname.NormalizeMinimally_(path); //the API supports .. etc if (!Api.GetFileAttributesEx(path, 0, out var d)) { if (!_GetAttributesOnError(path, flags, out _, out _, &d)) return false; } properties.Attributes = d.dwFileAttributes; properties.Size = d.Size; properties.LastWriteTimeUtc = DateTime.FromFileTimeUtc(d.ftLastWriteTime); properties.CreationTimeUtc = DateTime.FromFileTimeUtc(d.ftCreationTime); properties.LastAccessTimeUtc = DateTime.FromFileTimeUtc(d.ftLastAccessTime); if (d.dwFileAttributes.Has(FileAttributes.ReparsePoint)) properties.IsNtfsLink = 0 != IsNtfsLink_(path); return true; } /// /// Gets file or directory attributes. /// /// false if the file/directory does not exist. /// Full path. Supports @"\.." etc. If flag UseRawPath not used, supports environment variables (see ). /// Receives attributes, or 0 if failed. /// /// Not full path (when not used flag UseRawPath). /// Failed. Not thrown if used flag DontThrow. /// /// Calls API GetFileAttributes. Supports (useful with flag DontThrow). /// For NTFS links, gets attributes of the link, not of its target. /// public static unsafe bool getAttributes(string path, out FileAttributes attributes, FAFlags flags = 0) { if (0 == (flags & FAFlags.UseRawPath)) path = pathname.NormalizeMinimally_(path); //the API supports .. etc var a = Api.GetFileAttributes(path); if (a == (FileAttributes)(-1)) return _GetAttributesOnError(path, flags, out attributes, out _); attributes = a; return true; } static unsafe bool _GetAttributesOnError(string path, FAFlags flags, out FileAttributes attr, out bool ntfsLink, Api.WIN32_FILE_ATTRIBUTE_DATA* p = null) { attr = 0; ntfsLink = false; var ec = lastError.code; switch (ec) { case Api.ERROR_FILE_NOT_FOUND or Api.ERROR_PATH_NOT_FOUND or Api.ERROR_NOT_READY: //note: no ERROR_BAD_NETPATH. Let's print, it can help to debug caller code. return false; case Api.ERROR_SHARING_VIOLATION: //eg c:\pagefile.sys. GetFileAttributes fails, but FindFirstFile succeeds. case Api.ERROR_ACCESS_DENIED: //probably in a protected directory. Then FindFirstFile fails, but try anyway. Or a file marked for deletion. var d = new Api.WIN32_FIND_DATA(); var hfind = Api.FindFirstFile(path, out d); if (hfind != -1) { Api.FindClose(hfind); attr = d.dwFileAttributes; if (p != null) { p->dwFileAttributes = d.dwFileAttributes; p->nFileSizeHigh = d.nFileSizeHigh; p->nFileSizeLow = d.nFileSizeLow; p->ftCreationTime = d.ftCreationTime; p->ftLastAccessTime = d.ftLastAccessTime; p->ftLastWriteTime = d.ftLastWriteTime; } ntfsLink = 0 != d.IsNtfsLink; return true; } lastError.code = ec; attr = (FileAttributes)(-1); break; default: Debug_.Print(lastError.messageFor(ec)); break; } if (0 != (flags & FAFlags.DontThrow)) return false; throw new AuException(ec, $"*get file attributes: '{path}'"); } /// /// Gets attributes. /// Returns false if INVALID_FILE_ATTRIBUTES or if relative path. No exceptions. /// static unsafe bool _GetAttributes(string path, out FileAttributes attr, out bool ntfsLink, bool useRawPath) { if (!useRawPath) path = pathname.NormalizeMinimally_(path, throwIfNotFullPath: false); //note: NormalizeMinimally_ does not remove \ at the end. The API succeeds if "C:\x\dir\" but fails if "C:\x\file\" (it's good). attr = Api.GetFileAttributes(path); if (attr != (FileAttributes)(-1)) ntfsLink = attr.Has(FileAttributes.ReparsePoint) && 0 != IsNtfsLink_(path); else if (!_GetAttributesOnError(path, FAFlags.DontThrow, out attr, out ntfsLink)) return false; if (!useRawPath && !pathname.isFullPath(path)) { lastError.code = Api.ERROR_FILE_NOT_FOUND; return false; } return true; } /// /// Sets file or directory attributes. /// /// false if the file/directory does not exist. /// Full path. Supports @"\.." etc. If flag UseRawPath not used, supports environment variables (see ). /// Attributes to set, add or remove. /// null (default) - set; true - add; false - remove. /// /// Not full path (when not used flag UseRawPath). /// Failed. Not thrown if used flag DontThrow. /// /// Calls API SetFileAttributes. /// For NTFS links, sets attributes of the link, not of its target. /// public static unsafe bool setAttributes(string path, FileAttributes attributes, bool? add = null, FAFlags flags = 0) { if (0 == (flags & FAFlags.UseRawPath)) path = pathname.NormalizeMinimally_(path); //the API supports .. etc var a = attributes; if (add != null) { var v = Api.GetFileAttributes(path); if (add.Value) a = v | attributes; else a = v & ~attributes; if (a == attributes) return true; } if (Api.SetFileAttributes(path, a)) return true; if (0 != (flags & FAFlags.DontThrow)) return false; throw new AuException(0, $"*set file attributes: '{path}'"); } /// /// Calls API FindFirstFile to determine whether path is a NTFS link, such as symbolic link or mount point. /// /// Raw path (does not normalize). /// -1 failed, 0 no, 1 symlink, 2 mount, 3 other. internal static int IsNtfsLink_(string path) { var hfind = Api.FindFirstFile(path, out var fd); if (hfind == -1) return -1; int R = fd.IsNtfsLink; Api.FindClose(hfind); return R; } /// /// Gets file or directory attributes as that tells whether it exists, is directory, readonly, hidden, system, NTFS link. See examples. /// /// Full path. Supports @"\.." etc. If useRawPath is false (default), supports environment variables (see ). Can be null. /// Pass path to the API as it is, without any normalizing and full-path checking. /// /// Supports . If you need exception when fails, instead call . /// Always use full path. If not full path: if useRawPath is false (default), can't find the file; if useRawPath is true, searches in "current directory". /// For NTFS links gets attributes of the link, not of the target; does not care whether its target exists. /// /// /// /// public static FAttr exists(string path, bool useRawPath = false) { if (_GetAttributes(path, out var a, out bool ntfsLink, useRawPath)) return new(a, true, ntfsLink); return new(0, (a == (FileAttributes)(-1)) ? null : false, false); } /// /// Gets file system entry type - file, directory, NTFS link, whether it exists and is accessible. /// Returns NotFound (0) if does not exist. Returns AccessDenied (< 0) if exists but this process cannot access it and get attributes. /// Calls API GetFileAttributes. /// /// Full path. Supports @"\.." etc. If useRawPath is false (default), supports environment variables (see ). Can be null. /// Pass path to the API as it is, without any normalizing and full-path checking. /// /// Supports . If you need exception when fails, instead call . /// Always use full path. If path is not full: if useRawPath is false (default) returns NotFound; if useRawPath is true, searches in "current directory". /// internal static unsafe FileIs_ ExistsAs_(string path, bool useRawPath = false) { if (!_GetAttributes(path, out var a, out bool ntfsLink, useRawPath)) { return (a == (FileAttributes)(-1)) ? FileIs_.AccessDenied : FileIs_.NotFound; } var R = a.Has(FileAttributes.Directory) ? FileIs_.Directory : FileIs_.File; if (ntfsLink) R |= (FileIs_)4; return R; } #endregion #region enumerate, search /// /// Finds file or directory and returns full path. /// /// null if not found. /// /// If the path argument is full path, calls and returns normalized path if exists, null if not. /// Else searches in these places: /// 1. dirs, if used. /// 2. . /// 3. Calls API SearchPath, which searches in the process directory, Windows system directories, current directory, PATH environment variable. The search order depends on API SetSearchPathMode or registry settings. /// 4. If path ends with ".exe", tries to get path from registry "App Paths" keys. /// /// Full or relative path or just filename with extension. Supports network paths too. /// 0 or more directories where to search. public static unsafe string searchPath(string path, params string[] dirs) { if (path.NE()) return null; if (pathname.isFullPathExpand(path, out string s, strict: false)) { if (exists(s)) return pathname.Normalize_(s, noExpandEV: true); return null; } if (dirs != null) { foreach (var d in dirs) { if (!pathname.isFullPathExpand(d, out s)) continue; s = pathname.combine(s, path); if (exists(s)) return pathname.Normalize_(s, noExpandEV: true); } } s = folders.ThisApp + path; if (exists(s)) return pathname.Normalize_(s, noExpandEV: true); if ((s = Api.SearchPath(null, path)) != null) return s; if (path.Ends(".exe", true) && path.FindAny(@"\/") < 0) { try { s = Registry.GetValue(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\App Paths\" + path, "", null) as string ?? Registry.GetValue(@"HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\App Paths\" + path, "", null) as string; if (s != null) { s = _PreparePath(s.Trim('"')); if (exists(s, true)) return s; } } catch (Exception ex) { Debug_.Print(ex); } } //maybe PATH env var changed since this or parent process started //rejected. LA updates at startup an on WM_SETTINGCHANGE, and that's enough. //if (more.UpdatePathEnvVar_()) // if ((s = Api.SearchPath(null, path)) != null) return s; return null; } //TODO2: add overload with flags to ignore current directory etc. /// /// Gets names and other info of files and subdirectories in the specified directory. /// /// An enumerable collection of objects. /// Full path of the directory. /// /// /// Callback function that is called for each file (but not subdirectory). Let it return true to include the file in results. /// Example: f => f.Name.Ends(".png", true). /// /// /// Callback function that is called for each subdirectory. Let it return flags: 1 - include the directory in results; 2 - include its children in results. /// The return value overrides flags and . /// Example: d => d.Name.Eqi("Debug") ? 0 : 3. /// /// /// Callback function that is called when fails to get children of a subdirectory, when using flag . /// Receives the subdirectory path. Can call and throw an exception. If does not throw, the enumeration continues as if the directory is empty. /// If errorHandler not used, then throws exception. See also: flag . /// /// directoryPath is invalid path or not full path. /// directoryPath directory does not exist. /// Failed to get children of directoryPath or of a subdirectory. /// /// Uses API FindFirstFile. /// /// By default gets only direct children. Use flag to get all descendants. /// /// The paths that this function gets are normalized, ie may not start with exact directoryPath string. Expanded environment variables (see ), "..", DOS path etc. Paths longer than have @"\\?\" prefix (see ). /// /// For NTFS links (symbolic links, mount points) gets link info, not target info. /// /// These errors are ignored: /// 1. Missing target directory of a NTFS link. /// 2. If used flag - access denied. /// /// When an error is ignored, the function works as if that [sub]directory is empty; does not throw exception and does not call errorHandler. /// /// Enumeration of a subdirectory starts immediately after the subdirectory itself is retrieved. /// public static IEnumerable enumerate(string directoryPath, FEFlags flags = 0, Func fileFilter = null, Func dirFilter = null, Action errorHandler = null ) { //tested 2021-04-30: much faster than Directory.EnumerateX in .NET 5. Faster JIT, and then > 2 times faster. //tested 2022-01-31: ~20% slower than Directory.EnumerateX in .NET 6. Not tested JIT. Never mind. //tested 2025-06-20: ~1% slower than Directory.EnumerateX in .NET 9. // It seems .NET uses undocumented API NtQueryDirectoryFile. //rejected: in this func use .NET FileSystemEnumerable. // Good: faster; familiar types. // Bad: something we need is missing or difficult to return or need a workaround. Eg easily detect NTFS links, get relative path, prevent recursion to NTFS link target. //TODO2: stuck at ~340000 files when recursively enumerating C:\ProgramData\Microsoft\Windows\Containers\Layers. // Even can't end the task process normally (access denied error). // Actually it just becomes very slow, and maybe would complete if I waited long enough. // Directory.EnumerateFileSystemEntries stuck at ~61000 files. // The TreeSize app at first it seems stuck too, but after long time completes and reports 752000 files. string path = directoryPath; if (0 == (flags & FEFlags.UseRawPath)) path = _PreparePath(path); path = path.RemoveSuffix('\\'); var d = new Api.WIN32_FIND_DATA(); IntPtr hfind = default; var stack = new Stack<_EDStackEntry>(); bool isFirst = true; FileAttributes attr = 0; int basePathLength = path.Length; //var redir = new FileSystemRedirection(); try { //if (0 != (flags & FEFlags.DisableRedirection)) redir.Disable(); for (; ; ) { if (isFirst) { isFirst = false; var path2 = ((path.Length <= pathname.maxDirectoryPathLength - 2) ? path : pathname.prefixLongPath(path)) + @"\*"; #if TEST_FINDFIRSTFILEEX hfind = Api.FindFirstFileEx(path2, Api.FINDEX_INFO_LEVELS.FindExInfoBasic, out d, 0, default, 0); //speed: FindFirstFileEx 0-2 % slower. FindExInfoBasic makes 0-2% faster. FIND_FIRST_EX_LARGE_FETCH makes 1-50% slower. #else hfind = Api.FindFirstFile(path2, out d); #endif if (hfind == -1) { hfind = default; var ec = lastError.code; //print.it(ec, lastError.messageFor(ec), path); bool itsOK = false; switch (ec) { case Api.ERROR_FILE_NOT_FOUND: case Api.ERROR_NO_MORE_FILES: case Api.ERROR_NOT_READY: //rare, because most directories have "." and "..". //FindFirstFileEx sets ERROR_NO_MORE_FILES for some. //FindFirstFile could set ERROR_FILE_NOT_FOUND (documented), but was never in my tests. //ERROR_NOT_READY when trying to access an ejected CD/DVD drive etc. itsOK = true; break; case Api.ERROR_ACCESS_DENIED: itsOK = 0 != (flags & FEFlags.IgnoreInaccessible); break; case Api.ERROR_PATH_NOT_FOUND: //the directory not found, or NTFS link target directory is missing case Api.ERROR_DIRECTORY: //it is file, not directory. Error text is "The directory name is invalid". case Api.ERROR_BAD_NETPATH: //eg \\COMPUTER\MissingFolder if (stack.Count == 0 && !exists(path, true).Directory) throw new DirectoryNotFoundException($"Directory not found: '{path}'. {lastError.messageFor(ec)}"); //itsOK = (attr & Api.FILE_ATTRIBUTE_REPARSE_POINT) != 0; itsOK = true; //or maybe the subdirectory was deleted after we retrieved it break; case Api.ERROR_INVALID_NAME: //eg contains invalid characters case Api.ERROR_BAD_NET_NAME: //eg \\COMPUTER if (stack.Count == 0) throw new ArgumentException(lastError.messageFor(ec)); itsOK = true; break; } if (!itsOK) { if (errorHandler == null || stack.Count == 0) throw new AuException(ec, $"*enumerate directory '{path}'"); Api.SetLastError(ec); //the above code possibly changed it, although currently it doesn't errorHandler(path); } } } else { if (!Api.FindNextFile(hfind, out d)) { Debug.Assert(lastError.code == Api.ERROR_NO_MORE_FILES); Api.FindClose(hfind); hfind = default; } } if (hfind == default) { if (stack.Count == 0) break; var t = stack.Pop(); hfind = t.hfind; path = t.path; continue; } var name = d.Name; if (name == null) continue; //".", ".." attr = d.dwFileAttributes; bool isDir = (attr & FileAttributes.Directory) != 0; if ((flags & FEFlags.SkipHidden) != 0 && (attr & FileAttributes.Hidden) != 0) continue; const FileAttributes hidSys = FileAttributes.Hidden | FileAttributes.System; if ((attr & hidSys) == hidSys) { if ((flags & FEFlags.SkipHiddenSystem) != 0) continue; //skip Recycle Bin etc. It is useless, prevents copying drives, etc. if (isDir && path.Ends(':')) { if (name.Eqi("$Recycle.Bin")) continue; if (name.Eqi("System Volume Information")) continue; if (name.Eqi("Recovery")) continue; } } var fullPath = path + @"\" + name; if (0 != (flags & FEFlags.NeedRelativePaths)) name = fullPath[basePathLength..]; //prepend @"\\?\" etc if need. Don't change fullPath length, because then would be difficult to get relative path. var fp2 = pathname.prefixLongPathIfNeed(fullPath); var r = new FEFile(name, fp2, d, stack.Count); //never mind, don't need for dirs if no dirFilter and is flag OnlyFiles if (isDir) { int inc = dirFilter != null ? dirFilter(r) : (flags.Has(FEFlags.OnlyFiles) ? 0 : 1) | (flags.Has(FEFlags.AllDescendants) ? 2 : 0); if (0 != (1 & inc)) yield return r; if (0 == (2 & inc)) continue; if (!flags.Has(FEFlags.RecurseNtfsLinks) && 0 != d.IsNtfsLink) continue; stack.Push(new _EDStackEntry() { hfind = hfind, path = path }); hfind = default; path = fullPath; isFirst = true; } else { if (fileFilter == null || fileFilter(r)) yield return r; } } } finally { if (hfind != default) Api.FindClose(hfind); while (stack.Count > 0) Api.FindClose(stack.Pop().hfind); //redir.Revert(); } } struct _EDStackEntry { internal IntPtr hfind; internal string path; } /// /// Gets names and other info of matching files in the specified directory. /// /// Full path of the directory. /// /// File name pattern. Format: [wildcard expression](xref:wildcard_expression). Used only for files, not for subdirectories. Can be null. /// Examples: ///
"*.png" (only png files), ///
"**m *.png||*.bmp" (only png and bmp files), ///
"**nm *.png||*.bmp" (all files except png and bmp), ///
@"**r \.html?$" (regular expression that matches .htm and .html files). /// /// Flags. The function also adds flag OnlyFiles. /// /// directoryPath is invalid path or not full path. /// Invalid pattern ("**options " or regular expression). /// /// directoryPath directory does not exist. /// Failed to get children of directoryPath or of a subdirectory. /// public static IEnumerable enumFiles(string directoryPath, string pattern = null, FEFlags flags = 0) { flags |= FEFlags.OnlyFiles; if (pattern == null) return enumerate(directoryPath, flags); wildex w = pattern; return enumerate(directoryPath, flags, f => w.Match(f.Name)); } /// /// Gets names and other info of matching subdirectories in the specified directory. /// /// Full path of the directory. /// /// Directory name pattern. Format: [wildcard expression](xref:wildcard_expression). Can be null. /// /// /// /// directoryPath is invalid path or not full path. /// Invalid pattern ("**options " or regular expression). /// /// directoryPath directory does not exist. /// Failed to get children of directoryPath or of a subdirectory. /// public static IEnumerable enumDirectories(string directoryPath, string pattern = null, FEFlags flags = 0) { if (pattern == null) return filesystem.enumerate(directoryPath, flags, _ => false); wildex w = pattern; int enumChildren = flags.Has(FEFlags.AllDescendants) ? 2 : 0; return filesystem.enumerate(directoryPath, flags, _ => false, d => (w.Match(d.Name) ? 1 : 0) | enumChildren); } #endregion #region move, copy, rename, delete enum _FileOp { Rename, Move, Copy, } static unsafe void _FileOperation(_FileOp op, bool into, string path1, string path2, FIfExists ifExists, FCFlags copyFlags = 0, Func copyFileFilter = null, Func copyDirFilter = null ) { string opName = (op == _FileOp.Rename) ? "rename" : ((op == _FileOp.Move) ? "move" : "copy"); path1 = _PreparePath(path1); var type1 = ExistsAs_(path1, true); if (type1 <= 0) throw new FileNotFoundException($"Failed to {opName}. File not found: '{path1}'"); if (op == _FileOp.Rename) { op = _FileOp.Move; if (pathname.isInvalidName(path2)) throw new ArgumentException($"Invalid filename: '{path2}'"); path2 = pathname.Combine_(_RemoveFilename(path1), path2); } else { string path2Parent; if (into) { path2Parent = _PreparePath(path2); path2 = pathname.Combine_(path2Parent, _GetFilename(path1)); } else { path2 = _PreparePath(path2); path2Parent = _RemoveFilename(path2, true); } if (path2Parent != null) { try { _CreateDirectory(path2Parent, pathIsPrepared: true); } catch (Exception ex) { throw new AuException($"*create directory: '{path2Parent}'", ex); } } } bool ok = false, copy = op == _FileOp.Copy, deleteSource = false, mergeDirectory = false; var del = new _SafeDeleteExistingDirectory(); try { if (ifExists == FIfExists.MergeDirectory && type1 != FileIs_.Directory) ifExists = FIfExists.Fail; if (ifExists == FIfExists.Fail) { //API will fail if exists. We don't use API flags 'replace existing'. } else { //Delete, RenameExisting, MergeDirectory //bool deleted = false; var existsAs = ExistsAs_(path2, true); bool existsAsDir = existsAs is FileIs_.Directory or FileIs_.NtfsLinkDirectory; switch (existsAs) { case FileIs_.NotFound: //deleted = true; break; case FileIs_.AccessDenied: break; default: if (more.isSameFile(path1, path2, useSymlink: true)) { //eg renaming "file.txt" to "FILE.txt" Debug_.Print("same file"); //deleted = true; //copy will fail, move will succeed } else if (ifExists == FIfExists.RenameNew) { path2 = pathname.makeUnique(path2, existsAsDir); } else if (ifExists == FIfExists.MergeDirectory && existsAsDir) { if (type1 is FileIs_.Directory or FileIs_.NtfsLinkDirectory) { //deleted = true; mergeDirectory = true; if (!copy) { copy = true; deleteSource = true; } } // else API will fail. We refuse to replace a directory with a file. } else if (ifExists == FIfExists.RenameExisting || existsAs == FileIs_.Directory) { //deleted = del.Rename(path2, ifExists == FIfExists.RenameExisting); //Rename to a temp name. Finally delete if ok (if !RenameExisting), undo if failed. //It also solves this problem: if we delete the directory now, need to ensure that it does not delete the source directory, which is quite difficult. } else { //deleted = 0 == _DeleteL(path2, existsAs == FileIs_.NtfsLinkDirectory); } break; } //if(!deleted) throw new AuException(Api.ERROR_FILE_EXISTS, $"*{opName}"); //don't need, later API will fail } if (!copy) { //note: don't use MOVEFILE_COPY_ALLOWED, because then moving directory to another drive fails with ERROR_ACCESS_DENIED and we don't know that the reason is different drive if (ok = Api.MoveFileEx(path1, path2, 0)) return; if (lastError.code == Api.ERROR_NOT_SAME_DEVICE) { copy = true; deleteSource = true; } } if (copy) { if (type1 == FileIs_.Directory) { try { _CopyDirectory(path1, path2, mergeDirectory, copyFlags, copyFileFilter, copyDirFilter); ok = true; } catch (Exception ex) when (op != _FileOp.Copy) { throw new AuException($"*{opName} '{path1}' to '{path2}'", ex); } } else { if (type1 == FileIs_.NtfsLinkDirectory) ok = Api.CreateDirectoryEx(path1, path2, default); else ok = Api.CopyFileEx(path1, path2, Api.COPY_FILE_FAIL_IF_EXISTS | Api.COPY_FILE_COPY_SYMLINK); } } if (!ok) throw new AuException(0, $"*{opName} '{path1}' to '{path2}'"); if (deleteSource) { try { _Delete(path1); } catch (Exception ex) { if (!path1.Ends(':')) //moving drive contents. Deleted contents but cannot delete drive. print.warning($"Failed to delete '{path1}' after copying it to another drive. {ex.Message}"); //throw new AuException("*move", ex); //don't. MoveFileEx also succeeds even if fails to delete source. } } } finally { //dialog.show(); del.Finally(ok); } } //note: if merge, the destination directory must exist static unsafe void _CopyDirectory(string path1, string path2, bool merge, FCFlags copyFlags, Func fileFilter, Func dirFilter ) { //FUTURE: add progressInterface parameter. Create a default interface implementation class that supports progress dialog and/or progress in taskbar button. Or instead create a ShellCopy function. //FUTURE: maybe add errorHandler parameter. Call it here when fails to copy, and also pass to Enumerate which calls it when fails to enum. //use intermediate array, and get it before creating path2 directory. It requires more memory, but is safer. Without it, eg bad things happen when copying a directory into itself. var edFlags = FEFlags.AllDescendants | FEFlags.NeedRelativePaths | FEFlags.UseRawPath | (FEFlags)copyFlags; var a = enumerate(path1, edFlags, fileFilter, dirFilter).ToArray(); if (copyFlags.Has(FCFlags.NoEmptyDirectories)) { for (int i = a.Length; --i >= 0;) { var f = a[i]; if (!f.IsDirectory) continue; if (i < a.Length - 1 && a[i + 1] is FEFile ff) { if (ff.Name.Starts(f.Name) && ff.Name.Eq(f.Name.Length, '\\')) continue; } a[i] = null; } } bool ok = false; string s1 = null, s2 = null; if (!merge) { if (!path1.Ends(@":\")) ok = Api.CreateDirectoryEx(path1, path2, default); if (!ok) ok = Api.CreateDirectory(path2, default); if (!ok) goto ge; } string prevParentDir = null; foreach (var f in a) { if (f == null) continue; s1 = f.FullPath; s2 = pathname.prefixLongPathIfNeed(path2 + f.Name); //print.it(s2); //create intermediate dirs if need, eg if dirFilter returned 2 (don't include the dir but include its children) //TODO3: CreateDirectoryEx if (f.Level > 0 && (fileFilter != null || dirFilter != null)) { int ifn = _FindFilename(s2) - 1; if (prevParentDir == null || !s2.AsSpan(0, ifn).Eq(prevParentDir)) { //optimize to avoid this code for each file prevParentDir = s2[..ifn]; _CreateDirectory(prevParentDir, pathIsPrepared: true); } } if (f.IsDirectory) { if (merge) switch (exists(s2, true)) { case 2: continue; //never mind: check NTFS link mismatch case 1: _DeleteL(s2, false); break; } ok = Api.CreateDirectoryEx(s1, s2, default); if (!ok && !f.IsNtfsLink) ok = Api.CreateDirectory(s2, default); } else { if (merge && getAttributes(s2, out var attr, FAFlags.DontThrow | FAFlags.UseRawPath)) { const FileAttributes badAttr = FileAttributes.ReadOnly | FileAttributes.Hidden; if (0 != (attr & FileAttributes.Directory)) _Delete(s2); else if (0 != (attr & badAttr)) Api.SetFileAttributes(s2, attr & ~badAttr); } uint fl = Api.COPY_FILE_COPY_SYMLINK; if (!merge) fl |= Api.COPY_FILE_FAIL_IF_EXISTS; ok = Api.CopyFileEx(s1, s2, fl); } if (!ok) { if (f.IsNtfsLink) { //To create or copy NTFS links, need SeCreateSymbolicLinkPrivilege privilege. //Admins have it, else this process cannot get it. //Debug_.Print($"failed to copy NTFS link '{s1}'. It's OK, skipped it. Error: {lastError.messageFor()}"); continue; } if (0 != (copyFlags & FCFlags.IgnoreInaccessible)) { if (lastError.code == Api.ERROR_ACCESS_DENIED) continue; } goto ge; } } return; ge: string se = $"*copy directory '{path1}' to '{path2}'"; if (s1 != null) se += $" ('{s1}' to '{s2}')"; throw new AuException(0, se); //never mind: wrong API error code if path1 and path2 is the same directory. } struct _SafeDeleteExistingDirectory { string _oldPath, _tempPath; bool _dontDelete; /// /// note: path must be normalized. /// internal bool Rename(string path, bool dontDelete = false) { if (path.Length > pathname.maxDirectoryPathLength - 10) path = pathname.prefixLongPath(path); string tempPath = null; int iFN = _FindFilename(path); string s1 = path[..iFN] + "old", s2 = " " + path[iFN..]; for (int i = 1; ; i++) { tempPath = s1 + i + s2; if (!exists(tempPath, true)) break; } if (!Api.MoveFileEx(path, tempPath, 0)) return false; _oldPath = path; _tempPath = tempPath; _dontDelete = dontDelete; return true; } internal bool Finally(bool succeeded) { if (_tempPath == null) return true; if (!succeeded) { if (!Api.MoveFileEx(_tempPath, _oldPath, 0)) return false; } else if (!_dontDelete) { try { _Delete(_tempPath); } catch { return false; } } _oldPath = _tempPath = null; return true; } } /// /// Renames file or directory. /// /// Full path. /// New name without path. Example: "name.txt". /// /// /// - path is not full path (see ). /// - newName is invalid filename. /// /// path not found. /// Failed. /// /// Uses API MoveFileEx. /// public static void rename(string path, string newName, FIfExists ifExists = FIfExists.Fail) { _FileOperation(_FileOp.Rename, false, path, newName, ifExists); } /// /// Moves (changes path of) file or directory. /// /// Full path. /// /// New full path. /// NOTE: It is not the new parent directory. See . /// /// /// path or newPath is not full path (see ). /// path not found. /// Failed. /// /// In most cases uses API MoveFileEx. It's fast, because don't need to copy files. /// In these cases copies/deletes: destination is on another drive; need to merge directories. /// When need to copy, does not copy security properties; sets default. /// Creates the destination directory if does not exist (see ). /// If path and newPath share the same parent directory, just renames the file. /// public static void move(string path, string newPath, FIfExists ifExists = FIfExists.Fail) { _FileOperation(_FileOp.Move, false, path, newPath, ifExists); } /// /// Moves file or directory into another directory. /// /// Full path. /// Full path of the new parent directory. /// /// /// - path or newDirectory is not full path (see ). /// - path is drive. To move drive content, use . /// /// path not found. /// Failed. /// /// In most cases uses API MoveFileEx. It's fast, because don't need to copy files. /// In these cases copies/deletes: destination is on another drive; need to merge directories. /// When need to copy, does not copy security properties; sets default. /// Creates the destination directory if does not exist (see ). /// public static void moveTo(string path, string newDirectory, FIfExists ifExists = FIfExists.Fail) { _FileOperation(_FileOp.Move, true, path, newDirectory, ifExists); } /// /// Copies file or directory. /// /// Full path. /// /// Full path of the destination. /// NOTE: It is not the new parent directory. See . /// /// /// Options used when copying directory. /// Callback function that decides which files to copy when copying directory. See . Note: this function uses . /// Callback function that decides which subdirectories to copy when copying directory. See . Note: this function uses . /// path or newPath is not full path (see ). /// path not found. /// Failed. /// /// Uses API CopyFileEx and CreateDirectoryEx. /// On Windows 7 does not copy security properties; sets default. /// Does not copy symbolic links (silently skips, no exception) if this process is not running as administrator. /// Creates the destination directory if does not exist (see ). /// public static void copy(string path, string newPath, FIfExists ifExists = FIfExists.Fail, FCFlags copyFlags = 0, Func fileFilter = null, Func dirFilter = null ) { _FileOperation(_FileOp.Copy, false, path, newPath, ifExists, copyFlags, fileFilter, dirFilter); } /// /// Copies file or directory into another directory. /// /// Full path of the new parent directory. /// /// - path or newDirectory is not full path (see ). /// - path is drive. To copy drive content, use . /// /// path not found. /// Failed. /// public static void copyTo(string path, string newDirectory, FIfExists ifExists = FIfExists.Fail, FCFlags copyFlags = 0, Func fileFilter = null, Func dirFilter = null ) { _FileOperation(_FileOp.Copy, true, path, newDirectory, ifExists, copyFlags, fileFilter, dirFilter); } /// /// Deletes file or directory if exists. /// /// true if deleted, false if failed (with flag CanFail), null if did not exist. /// Full path. /// /// path is not full path (see ). /// Failed. /// /// Does nothing if it does not exist (no exception). /// If directory, also deletes all its files and subdirectories. If fails to delete some, tries to delete as many as possible. /// Deletes read-only files too. /// Does not show any message boxes etc (confirmation, error, UAC consent, progress). /// /// Some reasons why this function can fail: /// 1. The file is open (in any process). Or a file in the directory is open. /// 2. This process does not have security permissions to access or delete the file or directory or some of its descendants. /// 3. The directory is (or contains) the "current directory" (in any process). /// public static bool? delete(string path, FDFlags flags = 0) => _Delete(_PreparePath(path), flags); /// /// Deletes multiple files or/and directories. /// /// true if deleted all, false if failed to delete all or some (with flag CanFail), null if none existed. /// string array, List or other collection. Full paths. /// /// path is not full path (see ). /// Failed to delete all or some items. The exception object contains one for each failed-to-delete item. /// /// This overload is faster when using Recycle Bin. /// If fails to delete some items specified in the list, deletes as many as possible. /// public static bool? delete(IEnumerable paths, FDFlags flags = 0) { if (flags.Has(FDFlags.RecycleBin)) { var a = new List(); foreach (var v in paths) { var s = _PreparePath(v); if (exists(s, true)) a.Add(s); } if (a.Count == 0) return null; if (_DeleteShell(null, true, a)) return true; Debug_.Print("_DeleteShell failed"); //if (flags.Has(FDFlags.CanFail)) return false; //no, the shell API does not try to delete other items if fails to delete some //flags &= ~FDFlags.RecycleBin; //no } bool? R = null; if (flags.Has(FDFlags.CanFail)) { foreach (var v in paths) { switch (delete(v, flags)) { case true: R ??= true; break; case false: R = false; break; } } return R; } else { List ae = null; foreach (var v in paths) { try { if (delete(v, flags) == true) R = true; } catch (AuException e1) { (ae ??= new()).Add(e1); } } return ae == null ? R : throw new AggregateException("Failed to delete.", ae); } } /// /// note: path must be normalized. /// static bool? _Delete(string path, FDFlags flags = 0) { bool canFail = flags.Has(FDFlags.CanFail); var type = ExistsAs_(path, true); if (type == FileIs_.NotFound) return null; if (type == FileIs_.AccessDenied) return canFail ? false : throw new AuException(0, $"*delete '{path}'"); if (flags.Has(FDFlags.RecycleBin)) { if (_DeleteShell(path, true)) return true; if (canFail) return false; Debug_.Print("_DeleteShell failed"); } int ec = 0; if (type == FileIs_.Directory) { var a = enumerate(path, FEFlags.AllDescendants | FEFlags.UseRawPath | FEFlags.IgnoreInaccessible).ToArray(); //print.it(a); for (int i = a.Length; --i >= 0;) { //directories always are before their files, and will be empty when deleting in reverse var f = a[i]; var at = f.Attributes; if (at.Has(FileAttributes.ReadOnly)) Api.SetFileAttributes(path, at & ~FileAttributes.ReadOnly); _DeleteL(f.FullPath, f.IsDirectory, canFail); //delete as many as possible } ec = _DeleteL(path, true, canFail); if (ec == 0) { //notify shell. Else, if it was open in Explorer, it shows an error message box. //Info: .NET does not notify; SHFileOperation does. ShellNotify_(Api.SHCNE_RMDIR, path); return true; } if (!canFail) { Debug_.Print("Using _DeleteShell."); if (_DeleteShell(path, false)) return true; } } else { ec = _DeleteL(path, type == FileIs_.NtfsLinkDirectory, canFail); if (ec == 0) return true; } if (exists(path, true)) return canFail ? false : throw new AuException(ec, $"*delete '{path}'"); //info: //RemoveDirectory fails if not empty. //Directory.Delete fails if a descendant is read-only, etc. //Also both fail if a [now deleted] subfolder containing files was open in Explorer. Workaround: sleep/retry. //_DeleteShell usually does not have these problems. But it is very slow. //But all fail if it is current directory in any process. If in current process, _DeleteShell succeeds; it makes current directory = its parent. return true; } static int _DeleteL(string path, bool dir, bool canFail = false) { //print.it(dir, path); if (dir ? Api.RemoveDirectory(path) : Api.DeleteFile(path)) return 0; var ec = lastError.code; if (ec == Api.ERROR_ACCESS_DENIED) { var a = Api.GetFileAttributes(path); if (a != (FileAttributes)(-1) && 0 != (a & FileAttributes.ReadOnly)) { Api.SetFileAttributes(path, a & ~FileAttributes.ReadOnly); if (dir ? Api.RemoveDirectory(path) : Api.DeleteFile(path)) return 0; ec = lastError.code; } } if (!canFail) { for (int i = 1; (ec is Api.ERROR_SHARING_VIOLATION or Api.ERROR_LOCK_VIOLATION or Api.ERROR_DIR_NOT_EMPTY) && i <= 50; i++) { //ERROR_DIR_NOT_EMPTY: see comments above about Explorer. Also fails in other cases, eg when a file was opened in a web browser. Debug_.PrintIf(i > 1, (ec == Api.ERROR_DIR_NOT_EMPTY ? "ERROR_DIR_NOT_EMPTY when empty. Retry " : "ERROR_SHARING_VIOLATION. Retry ") + i.ToString()); Thread.Sleep(15); if (dir ? Api.RemoveDirectory(path) : Api.DeleteFile(path)) return 0; ec = lastError.code; } } if (ec is Api.ERROR_FILE_NOT_FOUND or Api.ERROR_PATH_NOT_FOUND) return 0; Debug_.Print("_DeleteL failed. " + lastError.messageFor(ec) + " " + path + (dir ? (" Children: " + string.Join(" | ", enumerate(path).Select(f => f.Name))) : null)); return ec; //never mind: .NET also calls DeleteVolumeMountPoint if it is a mount point. Somehow only for recursed dirs. // But I did not find in MSDN doc that need to do it before calling removedirectory. I think OS should unmount automatically. // Tested on Win10, works without unmounting explicitly. Even Explorer updates its current folder without notification. } static bool _DeleteShell(string path, bool recycle, List a = null) { if (a != null) path = string.Join("\0", a); if (wildex.hasWildcardChars(path)) throw new ArgumentException("*? not supported."); var x = new Api.SHFILEOPSTRUCT() { wFunc = Api.FO_DELETE }; uint f = Api.FOF_NO_UI; //info: FOF_NO_UI includes 4 flags - noerrorui, silent, noconfirm, noconfirmmkdir if (recycle) f |= Api.FOF_ALLOWUNDO; else f |= Api.FOF_NO_CONNECTED_ELEMENTS; x.fFlags = (ushort)f; x.pFrom = path + "\0"; x.hwnd = wnd.getwnd.root; //Call SHFileOperation in another thread. Because it pumps messages, including WM_PAINT and WM_TIMER, which causes random bugs. Also it's better to always call it in a STA thread. // However .NET wait methods pump messages too, although less. To reproduce, try to delete eg 10 files. //int r = Api.SHFileOperation(x); //int r = Task.Run(() => Api.SHFileOperation(x)).Result_(); //slower //int r = Task.Factory.StartNew(() => Api.SHFileOperation(x), default, 0, StaTaskScheduler_.Default).Result_(); //run.thread(() => { Api.SHFileOperation(x); }).Join(); using var wh = run.thread(out _, out _, () => { try { Api.SHFileOperation(x); } catch (Exception ex) { Debug_.Print(ex); } }); wait.forHandle(0, 0, wh.DangerousGetHandle()); //if(r != 0 || x.fAnyOperationsAborted) return false; //do not use fAnyOperationsAborted, it can be true even if finished to delete. Also, I guess it cannot be aborted because there is no UI, because we use FOF_SILENT to avoid deactivating the active window even when the UI is not displayed. //if(r != 0) return false; //after all, I don't trust this too //in some cases API returns 0 but does not delete. For example when path too long. if (a == null) { if (exists(path, true)) return false; } else { foreach (var v in a) if (exists(v, true)) return false; } return true; //Also tested IFileOperation, but it is even slower. //Unsuccessfully tried to add flag 'if cannot use Recycle Bin, show a Yes|No message box and delete or fail'. // FOF_WANTNUKEWARNING does not work as it should, eg is ignored when the file is in a flash drive. // It works only without FOF_NOCONFIRMATION, but then always shows confirmation if not disabled in RB Properties. } /// /// Creates new directory if does not exists. /// If need, creates missing parent/ancestor directories. /// /// true if created new directory, false if the directory already exists. Throws exception if failed. /// Path of new directory. /// Optional path of a template directory from which to copy some properties. See API CreateDirectoryEx. /// Not full path. /// Failed. /// /// If the directory already exists, this function does nothing, and returns false. /// Else, at first it creates missing parent/ancestor directories, then creates the specified directory. /// To create the specified directory, calls API CreateDirectory or CreateDirectoryEx (if templateDirectory is not null). /// public static bool createDirectory(string path, string templateDirectory = null) { return _CreateDirectory(path, templateDirectory: templateDirectory); } /// /// Creates parent directory for a new file, if does not exist. /// The same as , just removes filename from filePath. /// /// true if created new directory, false if the directory already exists. Throws exception if failed. /// Path of new file. /// Not full path. No filename. /// Failed. /// /// /// public static bool createDirectoryFor(string filePath) { filePath = _PreparePath(filePath); var path = _RemoveFilename(filePath); return _CreateDirectory(path, pathIsPrepared: true); } /// /// Same as , but filePath must be prepared (_PreparePath or normalize). /// static bool _createDirectoryForPrepared(string filePath) { var path = _RemoveFilename(filePath); return _CreateDirectory(path, pathIsPrepared: true); } static bool _CreateDirectory(string path, bool pathIsPrepared = false, string templateDirectory = null) { if (exists(path, pathIsPrepared).Directory) return false; if (!pathIsPrepared) path = _PreparePath(path); if (templateDirectory != null) templateDirectory = _PreparePath(templateDirectory); var stack = new Stack(); var s = path; do { stack.Push(s); s = _RemoveFilename(s, true); if (s == null) throw new AuException($@"*create directory '{path}'. Drive not found."); } while (!exists(s, true).Directory); while (stack.Count > 0) { s = stack.Pop(); int retry = 0; g1: bool ok = (templateDirectory == null || stack.Count > 0) ? Api.CreateDirectory(s, default) : Api.CreateDirectoryEx(templateDirectory, s, default); if (!ok) { int ec = lastError.code; if (ec == Api.ERROR_ALREADY_EXISTS) continue; if (ec == Api.ERROR_ACCESS_DENIED && ++retry < 5) { Thread.Sleep(15); goto g1; } //sometimes access denied briefly, eg immediately after deleting the folder while its parent is open in Explorer. Now could not reproduce on Win10. throw new AuException(0, $@"*create directory '{path}'"); } } return true; } internal static void ShellNotify_(uint @event, string path, string path2 = null) { //ThreadPool.QueueUserWorkItem(_ => Api.SHChangeNotify(@event, Api.SHCNF_PATH, path, path2)); //no, this process may end soon Api.SHChangeNotify(@event, Api.SHCNF_PATH, path, path2); //TODO3: test speed. If slow, use threadpool and the process exit event. } /// /// The same as pathname.normalize(path). /// Expands environment variables, throws ArgumentException if not full path, normalizes, etc. /// /// Not full path. static string _PreparePath(string path) { if (!pathname.isFullPathExpand(ref path)) throw new ArgumentException($"Not full path: '{path}'."); return pathname.Normalize_(path, noExpandEV: true); } /// /// Finds filename, eg @"b.txt" in @"c:\a\b.txt". /// /// '\\' not found or is at the end. If noException, instead returns -1. static int _FindFilename(string path, bool noException = false) { int R = path.FindLastAny(@"\/"); if (R < 0 || R == path.Length - 1) { if (noException) return -1; throw new ArgumentException($"No filename in path: '{path}'."); } return R + 1; } /// /// Removes filename, eg @"c:\a\b.txt" -> @"c:\a". /// /// '\\' not found or is at the end. If noException, instead returns null. static string _RemoveFilename(string path, bool noException = false) { int i = _FindFilename(path, noException); if (i < 0) return null; return path[..--i]; } /// /// Gets filename, eg @"c:\a\b.txt" -> @"b.txt". /// /// '\\' not found or is at the end. If noException, instead returns null. static string _GetFilename(string path, bool noException = false) { int i = _FindFilename(path, noException); if (i < 0) return null; return path[i..]; } #endregion #region load, save /// /// This function can be used to safely open a file that may be temporarily locked (used by another process or thread). Waits while the file is locked. /// /// The return value of the lambda f. /// Lambda that calls a function that creates, opens or opens/reads/closes a file. /// Wait max this number of milliseconds. Can be (-1). /// millisecondsTimeout less than -1. /// Exceptions thrown by the called function. /// /// Calls the lambda and handles . If the exception indicates that the file is locked, waits and retries in loop. /// /// /// File.ReadAllText(file)); //safe. Waits while the file is locked. /// /// using(var f = filesystem.waitIfLocked(() => File.OpenText(file))) { //safe. Waits while the file is locked. /// var s3 = f.ReadToEnd(); /// } /// /// using(var f = filesystem.waitIfLocked(() => File.Create(file))) { //safe. Waits while the file is locked. /// f.WriteByte(1); /// } /// ]]> /// public static T waitIfLocked(Func f, int millisecondsTimeout = 2000) { var w = new _LockedWaiter(millisecondsTimeout); g1: try { return f(); } catch (IOException e) when (w.ExceptionFilter(e)) { w.Sleep(); goto g1; } } /// /// /// File.WriteAllText(file, "TEXT")); //safe. Waits while the file is locked. /// ]]> /// /// public static void waitIfLocked(Action f, int millisecondsTimeout = 2000) { var w = new _LockedWaiter(millisecondsTimeout); g1: try { f(); } catch (IOException e) when (w.ExceptionFilter(e)) { w.Sleep(); goto g1; } } struct _LockedWaiter { int _timeout; long _t0; [MethodImpl(MethodImplOptions.AggressiveInlining)] public _LockedWaiter(int millisecondsTimeout) { if (millisecondsTimeout < -1) throw new ArgumentOutOfRangeException(); _timeout = millisecondsTimeout; _t0 = perf.ms; } public bool ExceptionFilter(IOException e) => ExceptionFilter(e.HResult & 0xffff); [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool ExceptionFilter(int ec) { //print.it((uint)ec); switch (ec) { case Api.ERROR_SHARING_VIOLATION: case Api.ERROR_LOCK_VIOLATION: case Api.ERROR_USER_MAPPED_FILE: case Api.ERROR_UNABLE_TO_REMOVE_REPLACED: //ReplaceFile or File.Replace return _timeout < 0 || perf.ms - _t0 < _timeout; default: return false; } } public void Sleep() => Thread.Sleep(15); } /// /// Loads text file in a safer way. /// Uses and . /// /// File. Must be full path. Can contain environment variables etc, see . /// Text encoding in file (if there is no BOM). Default UTF-8. /// If cannot open the file because it is opened by another process etc, wait max this number of milliseconds. Can be (-1). /// If the file initially does not exist, wait max this number of milliseconds until exists. Can be (-1). /// Not full path. /// A timeout is less than -1. /// missingWaitMS is > 0 and the file still does not exist after waiting. /// Exceptions of . public static string loadText(string file, Encoding encoding = null, int lockedWaitMS = 2000, int missingWaitMS = 0) { file = _LoadIntro(file, missingWaitMS); return waitIfLocked(() => encoding == null ? File.ReadAllText(file) : File.ReadAllText(file, encoding), lockedWaitMS); } static string _LoadIntro(string file, int ms) { file = pathname.NormalizeMinimally_(file); if (ms != 0) { double t = ms > 0 ? ms / 1000d : ms == -1 ? 0d : throw new ArgumentOutOfRangeException(); wait.until(t, () => exists(file, useRawPath: true)); } return file; } /// /// Loads file in a safer way. /// Uses and . /// /// File. Must be full path. Can contain environment variables etc, see . /// If cannot open the file because it is opened by another process etc, wait max this number of milliseconds. Can be (-1). /// If the file initially does not exist, wait max this number of milliseconds until exists. Can be (-1). /// Not full path. /// A timeout is less than -1. /// missingWaitMS is > 0 and the file still does not exist after waiting. /// Exceptions of . public static byte[] loadBytes(string file, int lockedWaitMS = 2000, int missingWaitMS = 0) { file = _LoadIntro(file, missingWaitMS); return waitIfLocked(() => File.ReadAllBytes(file), lockedWaitMS); } /// /// Loads file in a safer way. /// Uses and . /// /// File. Must be full path. Can contain environment variables etc, see . /// If cannot open the file because it is opened by another process etc, wait max this number of milliseconds. Can be (-1). /// If the file initially does not exist, wait max this number of milliseconds until exists. Can be (-1). /// Not full path. /// A timeout is less than -1. /// missingWaitMS is > 0 and the file still does not exist after waiting. /// Exceptions of . public static FileStream loadStream(string file, int lockedWaitMS = 2000, int missingWaitMS = 0) { file = _LoadIntro(file, missingWaitMS); return waitIfLocked(() => File.OpenRead(file), lockedWaitMS); } /// /// Writes any data to a file in a safe way, using a callback function. /// /// /// File. Must be full path. Can contain environment variables etc, see . /// If the file exists, this function overwrites it. If the directory does not exist, this function creates it. /// /// /// Callback function (lambda etc) that creates/writes/closes a temporary file. Its parameter is the full path of the temporary file, which normally does not exist. /// May be called multiple times, because this function retries if the file is locked or if the directory does not exist (if writer throws exception). /// /// Create backup file named file + "~backup". /// /// Directory for backup file and temporary file. If null or "" - file's directory. Can contain environment variables etc. /// Must be in the same drive as file. If the directory does not exist, this function creates it. /// If cannot open the file because it is opened by another process etc, wait max this number of milliseconds. Can be (-1). /// Invalid file (eg not full path) or lockedWaitMS (less than -1). /// Failed to replace file. /// Exceptions of the function that actually writes data. /// /// The file-write functions provided by .NET and Windows API are less reliable, because: /// 1. Fails if the file is temporarily open by another process or thread without sharing. /// 2. Can corrupt file data. If this thread, process, PC or disk dies while writing, may write only part of data or just make empty file. Usually it happens when PC is turned off incorrectly. /// /// To protect from 1, this functions waits/retries if the file is temporarily open/locked, like . /// To protect from 2, this function writes to a temporary file and renames/replaces the specified file using API ReplaceFile. Although not completely atomic, it ensures that file data is not corrupt; if cannot write all data, does not change existing file data. /// Also this function auto-creates directory if does not exist. /// /// This function is slower. Speed can be important when saving many files. /// public static void save(string file, Action writer, bool backup = false, string tempDirectory = null, int lockedWaitMS = 2000) { Not_.Null(writer); _Save(file, writer, backup, tempDirectory, lockedWaitMS); } /// /// Writes text to a file in a safe way (like ), using . /// /// Text to write. /// Text encoding in file. Default is UTF-8 without BOM. /// public static void saveText(string file, string text, bool backup = false, string tempDirectory = null, int lockedWaitMS = 2000, Encoding encoding = null) { _Save(file, text ?? "", backup, tempDirectory, lockedWaitMS, encoding); } /// /// Writes data to a file in a safe way (like ), using . /// /// Data to write. /// public static void saveBytes(string file, byte[] bytes, bool backup = false, string tempDirectory = null, int lockedWaitMS = 2000) { Not_.Null(bytes); _Save(file, bytes, backup, tempDirectory, lockedWaitMS); } //TODO2: ReadOnlySpan. static void _Save(string file, object data, bool backup, string tempDirectory, int lockedWaitMS, Encoding encoding = null) { file = _PreparePath(file); string s1 = file; if (!tempDirectory.NE()) { s1 = _PreparePath(tempDirectory); _CreateDirectory(s1, pathIsPrepared: true); s1 = pathname.combine(s1, pathname.getName(file)); } _createDirectoryForPrepared(file); string temp = s1 + "~temp~"; string back = s1 + "~backup~"; //always use the backup parameter, then ERROR_UNABLE_TO_REMOVE_REPLACED is far not so frequent, etc var w = new _LockedWaiter(lockedWaitMS); g1: try { switch (data) { case string text: //File.WriteAllText(temp, text, encoding ?? Encoding.UTF8); //no, it saves with BOM if (encoding != null) File.WriteAllText(temp, text, encoding); else File.WriteAllText(temp, text); break; case byte[] bytes: File.WriteAllBytes(temp, bytes); break; case Action func: func(temp); break; } } catch (IOException e) when (w.ExceptionFilter(e)) { w.Sleep(); goto g1; } w = new _LockedWaiter(lockedWaitMS); g2: string es = null; if (exists(file, true).File) { if (!Api.ReplaceFile(file, temp, back, 6)) es = "save"; //random ERROR_UNABLE_TO_REMOVE_REPLACED; _LockedWaiter knows it else if (backup) ShellNotify_(Api.SHCNE_RENAMEITEM, temp, file); //without it Explorer shows 2 files with filename of temp else if (!Api.DeleteFile(back)) Debug_.PrintNativeError(); //maybe should wait/retry if failed, but never noticed } else { if (!Api.MoveFileEx(temp, file, Api.MOVEFILE_REPLACE_EXISTING)) es = "create"; } if (es != null) { int ec = lastError.code; if (w.ExceptionFilter(ec)) { w.Sleep(); goto g2; } throw new IOException($"Failed to {es} file '{file}'. {lastError.messageFor(ec)}", ec); } } #endregion } ================================================ FILE: Au/Files, data/filesystem.more.cs ================================================ namespace Au; partial class filesystem { /// /// Miscellaneous rarely used file/directory functions. /// public static partial class more { /// /// Returns true if two paths are of the same existing file, regardless of path format etc. /// /// Path of a file or directory. Supports environment variables (see ) and paths relative to current directory. /// Path of a file or directory. Supports environment variables (see ) and paths relative to current directory. /// If a path is of a symbolic link, use the link. If false, uses its target; for example, returns false if the target doesn't exist. /// Not full path. /// public static bool isSameFile(string path1, string path2, bool useSymlink = false) { using var h1 = _OpenFileHandleForFileInfo(path1, useSymlink); if (h1.Is0) return false; using var h2 = _OpenFileHandleForFileInfo(path2, useSymlink); if (h2.Is0) return false; return Api.GetFileInformationByHandle(h1, out var k1) && Api.GetFileInformationByHandle(h2, out var k2) && k1.FileIndex == k2.FileIndex && k1.dwVolumeSerialNumber == k2.dwVolumeSerialNumber; } /// /// Gets of a file or directory. /// /// false if failed. Supports . /// Path of a file or directory. Supports environment variables (see ) and paths relative to current directory. /// /// If path is of a symbolic link, get of the link, not of its target. /// Not full path. /// /// Calls API GetFileInformationByHandle. /// /// A file id can be used to uniquely identify a file or directory regardless of path format. /// /// Note: later the function can get a different id for the same path. For example after deleting the file and then creating new file at the same path (some apps save files in this way). You may want to use instead. /// public static unsafe bool getFileId(string path, out FileId fileId, bool ofSymlink = false) { using var h = _OpenFileHandleForFileInfo(path, ofSymlink); if (h.Is0 || !Api.GetFileInformationByHandle(h, out var k)) { fileId = default; return false; } fileId = new((int)k.dwVolumeSerialNumber, k.FileIndex); return true; } static Handle_ _OpenFileHandleForFileInfo(string path, bool ofSymlink = false) { path = pathname.NormalizeMinimally_(path, throwIfNotFullPath: false); return Api.CreateFile(path, 0, Api.FILE_SHARE_ALL, Api.OPEN_EXISTING, ofSymlink ? Api.FILE_FLAG_BACKUP_SEMANTICS | Api.FILE_FLAG_OPEN_REPARSE_POINT : Api.FILE_FLAG_BACKUP_SEMANTICS); //info: need FILE_FLAG_BACKUP_SEMANTICS for directories. Ignored for files. } /// /// Gets full normalized path of an existing file or directory or symbolic link target. /// /// false if failed. For example if the path does not point to an existing file or directory. Supports . /// Full or relative path. Supports environment variables (see ). /// Receives full path, or null if failed. /// If path is of a symbolic link, get final path of the link, not of its target. /// Result format. /// Not full path. /// /// Calls API GetFinalPathNameByHandle. /// /// Unlike , this function works with existing files and directories, not any strings. /// /// public static bool getFinalPath(string path, out string result, bool ofSymlink = false, FPFormat format = FPFormat.PrefixIfLong) { result = null; using var h = _OpenFileHandleForFileInfo(path, ofSymlink); if (h.Is0 || !Api.GetFinalPathNameByHandle(h, out var s, format == FPFormat.VolumeGuid ? 1u : 0u)) return false; if (format == FPFormat.PrefixNever || (format == FPFormat.PrefixIfLong && s.Length <= pathname.maxDirectoryPathLength)) s = pathname.unprefixLongPath(s); result = s; return true; //never mind: does not change the root if it is like @"\\ThisComputer\share" or @"\\ThisComputer\C$" or @"\\127.0.0.1\c$" or @"\\LOCALHOST\c$" and it is the same as "C:\". // Tested: getFileId returns the same value for all these. } /// /// Compares final paths of two existing files or directories to determine equality or relationship. /// /// Full or relative path of an existing file or directory, in any format. Supports environment variables (see ). /// Full or relative path of an existing file or directory, in any format. Supports environment variables (see ). /// If pathA is of a symbolic link, get final path of the link, not of its target. /// If pathB is of a symbolic link, get final path of the link, not of its target. /// Not full path. /// /// Before comparing, calls , therefore paths can have any format. /// Example: @"C:\Test\" and @"C:\A\..\Test" are equal. /// Example: @"C:\Test\file.txt" and "file.txt" are equal if the file is in @"C:\Test and @"C:\Test is current directory. /// Example: @"C:\Temp\file.txt" and "%TEMP%\file.txt" are equal if TEMP is an environment variable = @"C:\Temp. /// /// public static CPResult comparePaths(string pathA, string pathB, bool ofSymlinkA = false, bool ofSymlinkB = false) => comparePaths(ref pathA, ref pathB, ofSymlinkA, ofSymlinkB); /// /// Compares final paths of two existing files or directories to determine equality or relationship. /// Also gets final paths (see ). /// /// public static CPResult comparePaths(ref string pathA, ref string pathB, bool ofSymlinkA = false, bool ofSymlinkB = false) { if (!getFinalPath(pathA, out pathA, ofSymlinkA, FPFormat.PrefixAlways)) return CPResult.Failed; if (!getFinalPath(pathB, out pathB, ofSymlinkB, FPFormat.PrefixAlways)) return CPResult.Failed; //print.it(pathA, pathB); if (pathA.Eqi(pathB)) return CPResult.Same; if (pathA.Length < pathB.Length && pathB.Starts(pathA, true) && (pathB[pathA.Length] == '\\' || pathA.Ends('\\'))) return CPResult.AContainsB; if (pathB.Length < pathA.Length && pathA.Starts(pathB, true) && (pathA[pathB.Length] == '\\' || pathB.Ends('\\'))) return CPResult.BContainsA; return CPResult.None; } /// /// Calls and returns the sum of all descendant file sizes. /// With default flags, it includes sizes of all descendant files, in this directory and all subdirectories except in inaccessible [sub]directories. /// /// Full path. /// flags. /// Exceptions of . By default no exceptions if used full path and the directory exists. /// /// This function is slow if the directory is large. /// Don't use this function for files (throws exception) and drives (instead use , it's fast and includes sizes of Recycle Bin and other protected hidden system directories). /// public static long calculateDirectorySize(string path, FEFlags flags = FEFlags.AllDescendants | FEFlags.IgnoreInaccessible) { return enumerate(path, flags).Sum(f => f.Size); } /// /// Empties the Recycle Bin. /// /// If not null, empties the Recycle Bin on this drive only. Example: "D:". /// Show progress dialog if slow. Default true. public static void emptyRecycleBin(string drive = null, bool progressUI = false) { Api.SHEmptyRecycleBin(default, drive, progressUI ? 1 : 7); } /// /// Creates a NTFS symbolic link or junction. /// /// Full path of the link. Supports environment variables etc. /// If type is Junction, must be full path. Else can be either full path or path relative to the parent directory of the link. If starts with an environment variable, the function expands it before creating the link. /// /// If fails to create symbolic link because this process does not have admin rights, run cmd.exe mklink as administrator. Will show a dialog and UAC consent. Not used if type is Junction, because don't need admin rights to create junctions. /// If linkPath already exists as a symbolic link or junction or empty directory, replace it. /// /// Some reasons why this function can fail: /// - The link already exists. Solution: use deleteOld: true. /// - This process is running not as administrator. Solution: use type Junction or elevate: true. To create symbolic links without admin rights, in Windows Settings enable developer mode. /// - The file system format is not NTFS. For example FAT32 in USB drive. /// /// More info: CreateSymbolicLink, mklink, NTFS symbolic links, junctions. /// /// Not full path. /// Failed. public static void createSymbolicLink(string linkPath, string targetPath, CSLink type, bool elevate = false, bool deleteOld = false) { linkPath = pathname.normalize(linkPath); if (type is CSLink.Junction) { targetPath = pathname.normalize(targetPath); //junctions don't support relative path } else { //symlinks support relative path targetPath = targetPath.Replace('/', '\\'); //rumors: the link may not work if with / } string trueLinkPath = null; try { if (exists(linkPath, useRawPath: true) is var e && e) { bool ok = deleteOld && (e.IsNtfsLink || (e.Directory && Api.RemoveDirectory(linkPath))); //info: remove empty dir because eg Explorer does not copy links, but istead creates empty dir if (!ok) throw new AuException(Api.ERROR_ALREADY_EXISTS, "*to create symbolic link."); trueLinkPath = linkPath; linkPath = pathname.makeUnique(linkPath, true); } else createDirectoryFor(linkPath); if (type is CSLink.Junction or CSLink.JunctionOrSymlink) { var r = run.console(out string s, "cmd.exe", $"""/u /c "mklink /d /j "{linkPath}" "{targetPath}" """, encoding: Encoding.Unicode); //tested: UTF-16 on Win11 and Win7 if (r == 0) return; if (!(type == CSLink.JunctionOrSymlink && s.Starts("Local volumes are required"))) throw new AuException("*to create junction. " + s.Trim()); } uint fl = type == CSLink.File ? 0u : 1u; //SYMBOLIC_LINK_FLAG_DIRECTORY if (osVersion.minWin10_1703) fl |= 2u; //SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE if (Api.CreateSymbolicLink(linkPath, targetPath, fl)) return; int ec = lastError.code; if (ec == Api.ERROR_PRIVILEGE_NOT_HELD && elevate && !uacInfo.isAdmin) { if (dialog.showOkCancel("Create symbolic link", "Administrator rights required.\n\nTo create without admin rights, in Windows Settings enable developer mode.", icon: DIcon.Shield)) { using var tf = new TempFile(); var d = type == CSLink.File ? null : "/d "; var cl = $"""/u /c "mklink {d}"{linkPath}" "{targetPath}" 2>"{tf}" """; //redirects stderr to temp file var r = run.it(folders.System + "cmd.exe", cl, RFlags.Admin | RFlags.WaitForExit, new() { WindowState = ProcessWindowStyle.Hidden }); if (r.ProcessExitCode == 0) return; string s = null; try { s = loadText(tf, encoding: Encoding.Unicode).Trim(); } catch { } throw new AuException("*to create symbolic link. " + s); } } throw new AuException(ec, "*to create symbolic link."); } finally { if (trueLinkPath != null && filesystem.exists(linkPath)) move(linkPath, trueLinkPath, FIfExists.Delete); } } /// /// Detects whether the path is on a SSD drive. /// /// Any path. The function uses just the drive part. Can be just "C:". Fails if a network path. /// true if succeeded and the path is on a SSD drive. /// /// Unreliable. For SSD drives usually returns true. For other drives usually fails and returns false. /// internal unsafe static bool IsOnSSD_(string path) { try { if (path.Starts(@"\\?\")) path = path[4..]; var di = new DriveInfo(path); if (!(di.DriveType is DriveType.Fixed or DriveType.Removable) || !di.IsReady) return false; var b = stackalloc char[300]; if (!Api.GetVolumeNameForVolumeMountPoint(di.Name, b, 300)) return false; path = new(Ptr_.ToRSpan(b).TrimEnd('\\')); } catch { return false; } using var h = Api.CreateFile(path, 0, Api.FILE_SHARE_READ | Api.FILE_SHARE_WRITE, Api.OPEN_EXISTING, Api.FILE_FLAG_BACKUP_SEMANTICS); if (h.Is0) return false; var query = new Api.STORAGE_PROPERTY_QUERY { PropertyId = 7 }; Api.DEVICE_SEEK_PENALTY_DESCRIPTOR spt = default; bool r = Api.DeviceIoControl(h, Api.IOCTL_STORAGE_QUERY_PROPERTY, &query, sizeof(Api.STORAGE_PROPERTY_QUERY), &spt, sizeof(Api.DEVICE_SEEK_PENALTY_DESCRIPTOR), out _); return r && spt.IncursSeekPenalty == 0; } #region garbage #if false //currently not used /// /// Gets HKEY_CLASSES_ROOT registry key of file type or protocol. /// The key usually contains subkeys shell, DefaultIcon, sometimes shellex and more. /// For example, for ".txt" can return "txtfile", for ".cs" - "VisualStudio.cs.14.0". /// Looks in HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\FileExts and in HKEY_CLASSES_ROOT. /// Returns null if the type/protocol is not registered. /// Returns null if fileType does not end with ".extension" and does not start with "protocol:"; also if starts with "shell:". /// /// /// File type extension like ".txt" or protocol like "http:". /// Can be full path or URL; the function gets extension or protocol from the string. /// Can start with %environment variable%. /// /// Don't parse fileType, it does not contain full path or URL or environment variables. It is ".ext" or "protocol:". /// fileType is URL or protocol like "http:". Used only if isFileType == true, ie it is protocol. internal static string getFileTypeOrProtocolRegistryKey(string fileType, bool isFileType, bool isURL) { if(!isFileType) fileType = GetExtensionOrProtocol(fileType, out isURL); else if(isURL) fileType = fileType.RemoveSuffix(1); //"proto:" -> "proto" if(fileType.NE()) return null; string userChoiceKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\FileExts\" + fileType + @"\UserChoice"; if(Registry.GetValue(userChoiceKey, "ProgId", null) is string s1) return s1; if(isURL) return fileType; if(Registry.ClassesRoot.GetValue(fileType, null) is string s2) return s2; return null; //note: IQueryAssociations.GetKey is very slow. } /// /// Gets file path extension like ".txt" or URL protocol like "http". /// Returns null if path does not end with ".extension" and does not start with "protocol:"; also if starts with "shell:". /// /// File path or URL. Can be just extension like ".txt" or protocol like "http:". /// Receives true if URL or protocol. internal static string GetExtensionOrProtocol(string path, out bool isProtocol) { isProtocol = false; if(path.NE()) return null; if(!PathIsExtension(path)) { int i = path.IndexOf(':'); if(i > 1) { path = path[..i]; //protocol if(path == "shell") return null; //eg "shell:AppsFolder\Microsoft.WindowsCalculator_8wekyb3d8bbwe!App" isProtocol = true; } else { path = pathname.getExtension(path); if(path.NE()) return null; } } return path; } #endif #if false //this is ~300 times slower than filesystem.move. SHFileOperation too. Use only for files or other shell items in virtual folders. Unfinished. public static void renameFileOrDirectory(string path, string newName) { perf.first(); if(pathname.isInvalidName(newName)) throw new ArgumentException("Invalid filename.", nameof(newName)); path = _PreparePath(path, nameof(path)); perf.next(); var si = _ShellItem(path, "*rename"); perf.next(); var fo = new api.FileOperation() as api.IFileOperation; perf.next(); try { fo.SetOperationFlags(4); //FOF_SILENT. Without it shows a hidden dialog that becomes the active window. AuException.ThrowIfFailed(fo.RenameItem(si, newName, null), "*rename"); perf.next(); AuException.ThrowIfFailed(fo.PerformOperations(), "*rename"); perf.next(); } finally { Api.ReleaseComObject(fo); Api.ReleaseComObject(si); } perf.nw(); } static api.IShellItem _ShellItem(string path, string errMsg) { var pidl = More.PidlFromString(path, true); try { var guid = typeof(api.IShellItem).GUID; AuException.ThrowIfFailed(api.SHCreateItemFromIDList(pidl, guid, out var R), errMsg); return R; } finally { Marshal.FreeCoTaskMem(pidl); } } static class api { [DllImport("shell32.dll", PreserveSig = true)] internal static extern int SHCreateItemFromIDList(IntPtr pidl, in Guid riid, out IShellItem ppv); [ComImport, Guid("3ad05575-8857-4850-9277-11b85bdb8e09"), ClassInterface(ClassInterfaceType.None)] internal class FileOperation { } [ComImport, Guid("947aab5f-0a5c-4c13-b4d6-4bf7836fc9f8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IFileOperation { [PreserveSig] int Advise(IFileOperationProgressSink pfops, out uint pdwCookie); [PreserveSig] int Unadvise(uint dwCookie); [PreserveSig] int SetOperationFlags(uint dwOperationFlags); [PreserveSig] int SetProgressMessage([MarshalAs(UnmanagedType.LPWStr)] string pszMessage); [PreserveSig] int SetProgressDialog(IOperationsProgressDialog popd); [PreserveSig] int SetProperties(IntPtr pproparray); //IPropertyChangeArray [PreserveSig] int SetOwnerWindow(wnd hwndOwner); [PreserveSig] int ApplyPropertiesToItem(IShellItem psiItem); [PreserveSig] int ApplyPropertiesToItems([MarshalAs(UnmanagedType.IUnknown)] Object punkItems); [PreserveSig] int RenameItem(IShellItem psiItem, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName, IFileOperationProgressSink pfopsItem); [PreserveSig] int RenameItems([MarshalAs(UnmanagedType.IUnknown)] Object pUnkItems, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName); [PreserveSig] int MoveItem(IShellItem psiItem, IShellItem psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName, IFileOperationProgressSink pfopsItem); [PreserveSig] int MoveItems([MarshalAs(UnmanagedType.IUnknown)] Object punkItems, IShellItem psiDestinationFolder); [PreserveSig] int CopyItem(IShellItem psiItem, IShellItem psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszCopyName, IFileOperationProgressSink pfopsItem); [PreserveSig] int CopyItems([MarshalAs(UnmanagedType.IUnknown)] Object punkItems, IShellItem psiDestinationFolder); [PreserveSig] int DeleteItem(IShellItem psiItem, IFileOperationProgressSink pfopsItem); [PreserveSig] int DeleteItems([MarshalAs(UnmanagedType.IUnknown)] Object punkItems); [PreserveSig] int NewItem(IShellItem psiDestinationFolder, uint dwFileAttributes, [MarshalAs(UnmanagedType.LPWStr)] string pszName, [MarshalAs(UnmanagedType.LPWStr)] string pszTemplateName, IFileOperationProgressSink pfopsItem); [PreserveSig] int PerformOperations(); [PreserveSig] int GetAnyOperationsAborted([MarshalAs(UnmanagedType.Bool)] out bool pfAnyOperationsAborted); } [ComImport, Guid("04b0f1a7-9490-44bc-96e1-4296a31252e2"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IFileOperationProgressSink { [PreserveSig] int StartOperations(); [PreserveSig] int FinishOperations(int hrResult); [PreserveSig] int PreRenameItem(uint dwFlags, IShellItem psiItem, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName); [PreserveSig] int PostRenameItem(uint dwFlags, IShellItem psiItem, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName, int hrRename, IShellItem psiNewlyCreated); [PreserveSig] int PreMoveItem(uint dwFlags, IShellItem psiItem, IShellItem psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName); [PreserveSig] int PostMoveItem(uint dwFlags, IShellItem psiItem, IShellItem psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName, int hrMove, IShellItem psiNewlyCreated); [PreserveSig] int PreCopyItem(uint dwFlags, IShellItem psiItem, IShellItem psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName); [PreserveSig] int PostCopyItem(uint dwFlags, IShellItem psiItem, IShellItem psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName, int hrCopy, IShellItem psiNewlyCreated); [PreserveSig] int PreDeleteItem(uint dwFlags, IShellItem psiItem); [PreserveSig] int PostDeleteItem(uint dwFlags, IShellItem psiItem, int hrDelete, IShellItem psiNewlyCreated); [PreserveSig] int PreNewItem(uint dwFlags, IShellItem psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName); [PreserveSig] int PostNewItem(uint dwFlags, IShellItem psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName, [MarshalAs(UnmanagedType.LPWStr)] string pszTemplateName, uint dwFileAttributes, int hrNew, IShellItem psiNewItem); [PreserveSig] int UpdateProgress(uint iWorkTotal, uint iWorkSoFar); [PreserveSig] int ResetTimer(); [PreserveSig] int PauseTimer(); [PreserveSig] int ResumeTimer(); } [ComImport, Guid("0C9FB851-E5C9-43EB-A370-F0677B13874C"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IOperationsProgressDialog { [PreserveSig] int StartProgressDialog(wnd hwndOwner, uint flags); [PreserveSig] int StopProgressDialog(); [PreserveSig] int SetOperation(SPACTION action); [PreserveSig] int SetMode(uint mode); [PreserveSig] int UpdateProgress(ulong ullPointsCurrent, ulong ullPointsTotal, ulong ullSizeCurrent, ulong ullSizeTotal, ulong ullItemsCurrent, ulong ullItemsTotal); [PreserveSig] int UpdateLocations(IShellItem psiSource, IShellItem psiTarget, IShellItem psiItem); [PreserveSig] int ResetTimer(); [PreserveSig] int PauseTimer(); [PreserveSig] int ResumeTimer(); [PreserveSig] int GetMilliseconds(out ulong pullElapsed, out ulong pullRemaining); [PreserveSig] int GetOperationStatus(out PDOPSTATUS popstatus); } internal enum SPACTION { SPACTION_NONE, SPACTION_MOVING, SPACTION_COPYING, SPACTION_RECYCLING, SPACTION_APPLYINGATTRIBS, SPACTION_DOWNLOADING, SPACTION_SEARCHING_INTERNET, SPACTION_CALCULATING, SPACTION_UPLOADING, SPACTION_SEARCHING_FILES, SPACTION_DELETING, SPACTION_RENAMING, SPACTION_FORMATTING, SPACTION_COPY_MOVING } internal enum PDOPSTATUS { PDOPS_RUNNING = 1, PDOPS_PAUSED, PDOPS_CANCELLED, PDOPS_STOPPED, PDOPS_ERRORS } } #endif //rejected: unreliable. Uses registry, where many mimes are incorrect and nonconstant. // Use System.Web.MimeMapping.GetMimeMapping. It uses a hardcoded list, although too small. ///// ///// Gets file's MIME content type, like "text/html" or "image/png". ///// Returns false if cannot detect it. ///// ///// File name or path or URL or just extension like ".txt". If canAnalyseData is true, must be full path of a file, and the file must exist and can be opened to read; else the function uses just .extension, and the file may exist or not. ///// Result. ///// If cannot detect from file extension, try to detect from file data. ///// Not full path. Only if canAnalyseData is true. ///// Exceptions of . Only if canAnalyseData is true. ///// ///// Uses API FindMimeFromData. ///// //public static bool getMimeContentType(string file, out string contentType, bool canAnalyseData = false) //{ // if(file.Ends(".cur", true)) { contentType = "image/x-icon"; return true; } //registered without MIME or with text/plain // int hr = Api.FindMimeFromData(default, file, null, 0, null, 0, out contentType, 0); // if(hr != 0 && canAnalyseData) { // file = pathname.normalize(file); // using(var stream = File.OpenRead(file)) { // var data = new byte[256]; // int n = stream.Read(data, 0, 256); // hr = Api.FindMimeFromData(default, null, data, n, null, 0, out contentType, 0); // } // } // return hr == 0; // //note: the returned unmanaged string is freed with CoTaskMemFree, which uses HeapFree(GetProcessHeap). // // In MSDN it is documented incorrectly: "should be freed with the operator delete function". // // To discover it, call HeapSize(GetProcessHeap) before and after CoTaskMemFree. It returns -1 when called after. //} #endregion } } ================================================ FILE: Au/Files, data/folders.cs ================================================ #pragma warning disable 1591 //missing XML documentation namespace Au { /// /// Gets known/special folder paths (Desktop, Temp, etc). /// /// /// Most functions return , not string. It is implicitly convertible to string. Its operator + appends a filename or relative path string, with \ separator if need. Example: /// /// /// /// If a function cannot get folder path, the return value contains null string. Then the + operator would throw . /// /// Some folders are known only on newer Windows versions or only on some computers. Some functions have a suffix like _Win8 which means that the folder is unavailable on older Windows. /// Some known folders, although supported and registered, may be still not created. /// /// Some folders are virtual, for example Control Panel. They don't have a file system path, but can be identified by a data structure ITEMIDLIST. Functions of the nested class return it as or string ":: ITEMIDLIST" that can be used with some functions of this library (, , ) but not with .NET functions. /// /// Most functions use Windows "Known Folders" API, such as SHGetKnownFolderPath. /// The list of Windows predefined known folders: KNOWNFOLDERID. /// Names of folders specific to current process have prefix This, like ThisApp. /// /// Some paths depend on the bitness (32 or 64 bit) of the OS and this process. /// /// /// /// /// /// /// /// /// /// /// /// /// ///
32-bit Windows /// System, SystemX86, SystemX64: @"C:\WINDOWS\system32" ///
ProgramFiles, ProgramFilesX86, ProgramFilesX64: @"C:\Program Files" ///
ProgramFilesCommon, ProgramFilesCommonX86, ProgramFilesCommonX64: @"C:\Program Files\Common Files" ///
64-bit Windows, 64-bit process /// System, SystemX64: @"C:\WINDOWS\system32" ///
SystemX86: @"C:\WINDOWS\SysWOW64" ///
ProgramFiles, ProgramFilesX64: @"C:\Program Files" ///
ProgramFilesX86: @"C:\Program Files (x86)" ///
ProgramFilesCommon, ProgramFilesCommonX64: @"C:\Program Files\Common Files" ///
ProgramFilesCommonX86: @"C:\Program Files (x86)\Common Files" ///
64-bit Windows, 32-bit process /// System: @"C:\WINDOWS\system32". However the OS in most cases redirects this path to @"C:\WINDOWS\SysWOW64". ///
SystemX86: @"C:\WINDOWS\SysWOW64" ///
SystemX64: @"C:\WINDOWS\Sysnative". The OS redirects it to the true @"C:\WINDOWS\system32". It is a special path, not in Explorer. ///
ProgramFiles, ProgramFilesX86: @"C:\Program Files (x86)" ///
ProgramFilesX64: @"C:\Program Files" ///
ProgramFilesCommon, ProgramFilesCommonX86: @"C:\Program Files (x86)\Common Files" ///
ProgramFilesCommonX64: @"C:\Program Files\Common Files" ///
///
public static class folders { #region generated by macro "Auto create special folders class from KNOWNFOLDERID" public static FolderPath AccountPictures_Win8 => _Get(0x008ca0b1, 0x55b44c56, 0xb8a84de4, 0xb299d3be); public static FolderPath AdminTools => _Get(0x724EF170, 0xA42D4FEF, 0x9F26B60E, 0x846FBA4F); public static FolderPath ApplicationShortcuts_Win8 => _Get(0xA3918781, 0xE5F24890, 0xB3D9A7E5, 0x4332328C); public static FolderPath CameraRoll_Win81 => _Get(0xAB5FB87B, 0x7CE24F83, 0x915D5508, 0x46C9537B); public static FolderPath CDBurning => _Get(0x9E52AB10, 0xF80D49DF, 0xACB84330, 0xF5687855); public static FolderPath CommonAdminTools => _Get(0xD0384E7D, 0xBAC34797, 0x8F14CBA2, 0x29B392B5); public static FolderPath CommonOEMLinks => _Get(0xC1BAE2D0, 0x10DF4334, 0xBEDD7AA2, 0x0B227A9D); public static FolderPath CommonPrograms => _Get(0x0139D44E, 0x6AFE49F2, 0x86903DAF, 0xCAE6FFB8); public static FolderPath CommonStartMenu => _Get(0xA4115719, 0xD62E491D, 0xAA7CE74B, 0x8BE3B067); public static FolderPath CommonStartup => _Get(0x82A5EA35, 0xD9CD47C5, 0x9629E15D, 0x2F714E6E); public static FolderPath CommonTemplates => _Get(0xB94237E7, 0x57AC4347, 0x9151B08C, 0x6C32D1F7); public static FolderPath Contacts => _Get(0x56784854, 0xC6CB462b, 0x816988E3, 0x50ACB882); public static FolderPath Cookies => _Get(0x2B0F765D, 0xC0E94171, 0x908E08A6, 0x11B84FF6); public static FolderPath Desktop => _Get(0xB4BFCC3A, 0xDB2C424C, 0xB0297FE9, 0x9A87C641); public static FolderPath DeviceMetadataStore => _Get(0x5CE4A5E9, 0xE4EB479D, 0xB89F130C, 0x02886155); public static FolderPath Documents => _Get(0xFDD39AD0, 0x238F46AF, 0xADB46C85, 0x480369C7); public static FolderPath DocumentsLibrary => _Get(0x7B0DB17D, 0x9CD24A93, 0x973346CC, 0x89022E7C); public static FolderPath Downloads => _Get(0x374DE290, 0x123F4565, 0x916439C4, 0x925E467B); public static FolderPath Favorites => _Get(0x1777F761, 0x68AD4D8A, 0x87BD30B7, 0x59FA33DD); public static FolderPath Fonts => _Get(0xFD228CB7, 0xAE114AE3, 0x864C16F3, 0x910AB8FE); public static FolderPath GameTasks => _Get(0x054FAE61, 0x4DD84787, 0x80B60902, 0x20C4B700); public static FolderPath History => _Get(0xD9DC8A3B, 0xB784432E, 0xA7815A11, 0x30A75963); public static FolderPath ImplicitAppShortcuts => _Get(0xBCB5256F, 0x79F64CEE, 0xB725DC34, 0xE402FD46); public static FolderPath InternetCache => _Get(0x352481E8, 0x33BE4251, 0xBA856007, 0xCAEDCF9D); public static FolderPath Libraries => _Get(0x1B3EA5DC, 0xB5874786, 0xB4EFBD1D, 0xC332AEAE); public static FolderPath Links => _Get(0xbfb9d5e0, 0xc6a9404c, 0xb2b2ae6d, 0xb6af4968); public static FolderPath LocalAppData => _Get(0xF1B32785, 0x6FBA4FCF, 0x9D557B8E, 0x7F157091); public static FolderPath LocalAppDataLow => _Get(0xA520A1A4, 0x17804FF6, 0xBD181673, 0x43C5AF16); public static FolderPath LocalizedResourcesDir => _Get(0x2A00375E, 0x224C49DE, 0xB8D1440D, 0xF7EF3DDC); public static FolderPath Music => _Get(0x4BD8D571, 0x6D1948D3, 0xBE974222, 0x20080E43); public static FolderPath MusicLibrary => _Get(0x2112AB0A, 0xC86A4FFE, 0xA3680DE9, 0x6E47012E); public static FolderPath NetHood => _Get(0xC5ABBF53, 0xE17F4121, 0x89008662, 0x6FC2C973); public static FolderPath OriginalImages => _Get(0x2C36C0AA, 0x58124b87, 0xBFD04CD0, 0xDFB19B39); public static FolderPath PhotoAlbums => _Get(0x69D2CF90, 0xFC334FB7, 0x9A0CEBB0, 0xF0FCB43C); public static FolderPath PicturesLibrary => _Get(0xA990AE9F, 0xA03B4E80, 0x94BC9912, 0xD7504104); public static FolderPath Pictures => _Get(0x33E28130, 0x4E1E4676, 0x835A9839, 0x5C3BC3BB); public static FolderPath Playlists => _Get(0xDE92C1C7, 0x837F4F69, 0xA3BB86E6, 0x31204A23); public static FolderPath PrintHood => _Get(0x9274BD8D, 0xCFD141C3, 0xB35EB13F, 0x55A758F4); public static FolderPath Profile => _Get(0x5E6C858F, 0x0E224760, 0x9AFEEA33, 0x17B67173); public static FolderPath ProgramData => _Get(0x62AB5D82, 0xFDC14DC3, 0xA9DD070D, 0x1D495D97); /// More info in class help. public static FolderPath ProgramFiles => new(__ProgramFiles ??= _ProgramFiles); static string __ProgramFiles; static FolderPath _ProgramFiles => _Get(0x905e63b6, 0xc1bf494e, 0xb29c65b7, 0x32d3d21a); //broken static FolderPath ProgramFilesX64 => _Get(0x6D809377, 0x6AF0444b, 0x8957A377, 0x3F02200E); /// More info in class help. public static FolderPath ProgramFilesX86 => new(__ProgramFilesX86 ??= _ProgramFilesX86); static string __ProgramFilesX86; static FolderPath _ProgramFilesX86 => _Get(0x7C5A40EF, 0xA0FB4BFC, 0x874AC0F2, 0xE0B9FA8E); /// More info in class help. public static FolderPath ProgramFilesCommon => _Get(0xF7F1ED05, 0x9F6D47A2, 0xAAAE29D3, 0x17C6F066); //broken static FolderPath ProgramFilesCommonX64 => _Get(0x6365D5A7, 0x0F0D45E5, 0x87F60DA5, 0x6B6A4F7D); /// More info in class help. public static FolderPath ProgramFilesCommonX86 => _Get(0xDE974D24, 0xD9C64D3E, 0xBF91F445, 0x5120B917); public static FolderPath Programs => _Get(0xA77F5D77, 0x2E2B44C3, 0xA6A2ABA6, 0x01054A51); public static FolderPath Public => _Get(0xDFDF76A2, 0xC82A4D63, 0x906A5644, 0xAC457385); public static FolderPath PublicDesktop => _Get(0xC4AA340D, 0xF20F4863, 0xAFEFF87E, 0xF2E6BA25); public static FolderPath PublicDocuments => _Get(0xED4824AF, 0xDCE445A8, 0x81E2FC79, 0x65083634); public static FolderPath PublicDownloads => _Get(0x3D644C9B, 0x1FB84f30, 0x9B45F670, 0x235F79C0); public static FolderPath PublicGameTasks => _Get(0xDEBF2536, 0xE1A84c59, 0xB6A24145, 0x86476AEA); public static FolderPath PublicLibraries => _Get(0x48DAF80B, 0xE6CF4F4E, 0xB8000E69, 0xD84EE384); public static FolderPath PublicMusic => _Get(0x3214FAB5, 0x97574298, 0xBB6192A9, 0xDEAA44FF); public static FolderPath PublicPictures => _Get(0xB6EBFB86, 0x6907413C, 0x9AF74FC2, 0xABF07CC5); public static FolderPath PublicRingtones => _Get(0xE555AB60, 0x153B4D17, 0x9F04A5FE, 0x99FC15EC); public static FolderPath PublicUserTiles_Win8 => _Get(0x0482af6c, 0x08f14c34, 0x8c90e17e, 0xc98b1e17); public static FolderPath PublicVideos => _Get(0x2400183A, 0x618549FB, 0xA2D84A39, 0x2A602BA3); public static FolderPath QuickLaunch => _Get(0x52a4f021, 0x7b7548a9, 0x9f6b4b87, 0xa210bc8f); public static FolderPath Recent => _Get(0xAE50C081, 0xEBD2438A, 0x86558A09, 0x2E34987A); public static FolderPath RecordedTV => _Get(0x1A6FDBA2, 0xF42D4358, 0xA798B74D, 0x745926C5); public static FolderPath ResourceDir => _Get(0x8AD10C31, 0x2ADB4296, 0xA8F7E470, 0x1232C972); public static FolderPath Ringtones => _Get(0xC870044B, 0xF49E4126, 0xA9C3B52A, 0x1FF411E8); public static FolderPath RoamingAppData => _Get(0x3EB685DB, 0x65F94CF6, 0xA03AE3EF, 0x65729F3D); public static FolderPath RoamedTileImages_Win8 => _Get(0xAAA8D5A5, 0xF1D64259, 0xBAA878E7, 0xEF60835E); public static FolderPath RoamingTiles_Win8 => _Get(0x00BCFC5A, 0xED944e48, 0x96A13F62, 0x17F21990); public static FolderPath SampleMusic => _Get(0xB250C668, 0xF57D4EE1, 0xA63C290E, 0xE7D1AA1F); public static FolderPath SamplePictures => _Get(0xC4900540, 0x23794C75, 0x844B64E6, 0xFAF8716B); public static FolderPath SamplePlaylists => _Get(0x15CA69B3, 0x30EE49C1, 0xACE16B5E, 0xC372AFB5); public static FolderPath SampleVideos => _Get(0x859EAD94, 0x2E8548AD, 0xA71A0969, 0xCB56A6CD); public static FolderPath SavedGames => _Get(0x4C5C32FF, 0xBB9D43b0, 0xB5B42D72, 0xE54EAAA4); public static FolderPath SavedPictures => _Get(0x3B193882, 0xD3AD4eab, 0x965A6982, 0x9D1FB59F); public static FolderPath SavedPicturesLibrary => _Get(0xE25B5812, 0xBE884bd9, 0x94B02923, 0x3477B6C3); public static FolderPath SavedSearches => _Get(0x7d1d3a04, 0xdebb4115, 0x95cf2f29, 0xda2920da); public static FolderPath Screenshots_Win8 => _Get(0xb7bede81, 0xdf944682, 0xa7d857a5, 0x2620b86f); public static FolderPath SearchHistory_Win81 => _Get(0x0D4C3DB6, 0x03A3462F, 0xA0E60892, 0x4C41B5D4); public static FolderPath SearchTemplates_Win81 => _Get(0x7E636BFE, 0xDFA94D5E, 0xB456D7B3, 0x9851D8A9); public static FolderPath SendTo => _Get(0x8983036C, 0x27C0404B, 0x8F08102D, 0x10DCFD74); public static FolderPath SidebarDefaultParts => _Get(0x7B396E54, 0x9EC54300, 0xBE0A2482, 0xEBAE1A26); public static FolderPath SidebarParts => _Get(0xA75D362E, 0x50FC4fb7, 0xAC2CA8BE, 0xAA314493); public static FolderPath SkyDrive_Win81 => _Get(0xA52BBA46, 0xE9E1435f, 0xB3D928DA, 0xA648C0F6); public static FolderPath SkyDriveCameraRoll_Win81 => _Get(0x767E6811, 0x49CB4273, 0x87C220F3, 0x55E1085B); public static FolderPath SkyDriveDocuments_Win81 => _Get(0x24D89E24, 0x2F194534, 0x9DDE6A66, 0x71FBB8FE); public static FolderPath SkyDrivePictures_Win81 => _Get(0x339719B5, 0x8C474894, 0x94C2D8F7, 0x7ADD44A6); public static FolderPath StartMenu => _Get(0x625B53C3, 0xAB484EC1, 0xBA1FA1EF, 0x4146FC19); public static FolderPath Startup => _Get(0xB97D20BB, 0xF46A4C97, 0xBA105E36, 0x08430854); /// More info in class help. public static FolderPath System => new(__System ??= _System); static string __System; static FolderPath _System => _Get(0x1AC14E77, 0x02E74E5D, 0xB7442EB1, 0xAE5198B7); /// More info in class help. public static FolderPath SystemX86 => _Get(0xD65231B0, 0xB2F14857, 0xA4CEA8E7, 0xC6EA7D27); public static FolderPath Templates => _Get(0xA63293E8, 0x664E48DB, 0xA079DF75, 0x9E0509F7); public static FolderPath TreeProperties => _Get(0x9E3995AB, 0x1F9C4F13, 0xB82748B2, 0x4B6C7174); public static FolderPath UserProfiles => _Get(0x0762D272, 0xC50A4BB0, 0xA382697D, 0xCD729B80); public static FolderPath UserProgramFiles => _Get(0x5CD7AEE2, 0x22194A67, 0xB85D6C9C, 0xE15660CB); public static FolderPath UserProgramFilesCommon => _Get(0xBCBD3057, 0xCA5C4622, 0xB42DBC56, 0xDB0AE516); public static FolderPath Videos => _Get(0x18989B1D, 0x99B5455B, 0x841CAB7C, 0x74E4DDFC); public static FolderPath VideosLibrary => _Get(0x491E922F, 0x56434AF4, 0xA7EB4E7A, 0x138D8174); public static FolderPath Windows => new(__Windows ??= _Windows); static string __Windows; static FolderPath _Windows => _Get(0xF38BF404, 0x1D4342F2, 0x930567DE, 0x0B28FC23); /// /// Gets ITEMIDLIST of known/special virtual folders (eg Control Panel), as string like ":: 12345678..." or as . /// public static class shell { public static FolderPath AddNewPrograms => _GetV(0xde61d971, 0x5ebc4f02, 0xa3a96c82, 0x895e5c04); public static FolderPath Apps_Win8 => _GetV(0x1e87508d, 0x89c242f0, 0x8a7e645a, 0x0f50ca58); public static FolderPath AppUpdates => _GetV(0xa305ce99, 0xf527492b, 0x8b1a7e76, 0xfa98d6e4); public static FolderPath ChangeRemovePrograms => _GetV(0xdf7266ac, 0x92744867, 0x8d553bd6, 0x61de872d); public static FolderPath Computer => _GetV(0x0AC0837C, 0xBBF8452A, 0x850D79D0, 0x8E667CA7); public static FolderPath Conflict => _GetV(0x4bfefb45, 0x347d4006, 0xa5beac0c, 0xb0567192); public static FolderPath Connections => _GetV(0x6F0CD92B, 0x2E9745D1, 0x88FFB0D1, 0x86B8DEDD); public static FolderPath ControlPanel => _GetV(0x82A74AEB, 0xAEB4465C, 0xA014D097, 0xEE346D63); public static FolderPath Games => _GetV(0xCAC52C1A, 0xB53D4edc, 0x92D76B2E, 0x8AC19434); public static FolderPath HomeGroup => _GetV(0x52528A6B, 0xB9E34ADD, 0xB60D588C, 0x2DBA842D); public static FolderPath HomeGroupCurrentUser_Win8 => _GetV(0x9B74B6A3, 0x0DFD4f11, 0x9E785F78, 0x00F2E772); public static FolderPath Internet => _GetV(0x4D9F7874, 0x4E0C4904, 0x967B40B0, 0xD20C3E4B); public static FolderPath Network => _GetV(0xD20BEEC4, 0x5CA84905, 0xAE3BBF25, 0x1EA09B53); public static FolderPath Printers => _GetV(0x76FC4E2D, 0xD6AD4519, 0xA66337BD, 0x56068185); public static FolderPath RecycleBin => _GetV(0xB7534046, 0x3ECB4C18, 0xBE4E64CD, 0x4CB7D6AC); public static FolderPath SEARCH_CSC => _GetV(0xee32e446, 0x31ca4aba, 0x814fa5eb, 0xd2fd6d5e); public static FolderPath SearchHome => _GetV(0x190337d1, 0xb8ca4121, 0xa6396d47, 0x2d16972a); public static FolderPath SEARCH_MAPI => _GetV(0x98ec0e18, 0x20984d44, 0x86446697, 0x9315a281); public static FolderPath SyncManager => _GetV(0x43668BF8, 0xC14E49B2, 0x97C97477, 0x84D784B7); public static FolderPath SyncResults => _GetV(0x289a9a43, 0xbe444057, 0xa41b587a, 0x76d7e7f9); public static FolderPath SyncSetup => _GetV(0x0F214138, 0xB1D34a90, 0xBBA927CB, 0xC0C5389A); public static FolderPath UsersFiles => _GetV(0xf3ce0f7c, 0x49014acc, 0x8648d5d4, 0x4b04ef8f); public static FolderPath UsersLibraries => _GetV(0xA302545D, 0xDEFF464b, 0xABE861C8, 0x648D939B); public static Pidl pidlAddNewPrograms => _GetVI(0xde61d971, 0x5ebc4f02, 0xa3a96c82, 0x895e5c04); public static Pidl pidlApps_Win8 => _GetVI(0x1e87508d, 0x89c242f0, 0x8a7e645a, 0x0f50ca58); public static Pidl pidlAppUpdates => _GetVI(0xa305ce99, 0xf527492b, 0x8b1a7e76, 0xfa98d6e4); public static Pidl pidlChangeRemovePrograms => _GetVI(0xdf7266ac, 0x92744867, 0x8d553bd6, 0x61de872d); public static Pidl pidlComputer => _GetVI(0x0AC0837C, 0xBBF8452A, 0x850D79D0, 0x8E667CA7); public static Pidl pidlConflict => _GetVI(0x4bfefb45, 0x347d4006, 0xa5beac0c, 0xb0567192); public static Pidl pidlConnections => _GetVI(0x6F0CD92B, 0x2E9745D1, 0x88FFB0D1, 0x86B8DEDD); public static Pidl pidlControlPanel => _GetVI(0x82A74AEB, 0xAEB4465C, 0xA014D097, 0xEE346D63); public static Pidl pidlGames => _GetVI(0xCAC52C1A, 0xB53D4edc, 0x92D76B2E, 0x8AC19434); public static Pidl pidlHomeGroup => _GetVI(0x52528A6B, 0xB9E34ADD, 0xB60D588C, 0x2DBA842D); public static Pidl pidlHomeGroupCurrentUser_Win8 => _GetVI(0x9B74B6A3, 0x0DFD4f11, 0x9E785F78, 0x00F2E772); public static Pidl pidlInternet => _GetVI(0x4D9F7874, 0x4E0C4904, 0x967B40B0, 0xD20C3E4B); public static Pidl pidlNetwork => _GetVI(0xD20BEEC4, 0x5CA84905, 0xAE3BBF25, 0x1EA09B53); public static Pidl pidlPrinters => _GetVI(0x76FC4E2D, 0xD6AD4519, 0xA66337BD, 0x56068185); public static Pidl pidlRecycleBin => _GetVI(0xB7534046, 0x3ECB4C18, 0xBE4E64CD, 0x4CB7D6AC); public static Pidl pidlSEARCH_CSC => _GetVI(0xee32e446, 0x31ca4aba, 0x814fa5eb, 0xd2fd6d5e); public static Pidl pidlSearchHome => _GetVI(0x190337d1, 0xb8ca4121, 0xa6396d47, 0x2d16972a); public static Pidl pidlSEARCH_MAPI => _GetVI(0x98ec0e18, 0x20984d44, 0x86446697, 0x9315a281); public static Pidl pidlSyncManager => _GetVI(0x43668BF8, 0xC14E49B2, 0x97C97477, 0x84D784B7); public static Pidl pidlSyncResults => _GetVI(0x289a9a43, 0xbe444057, 0xa41b587a, 0x76d7e7f9); public static Pidl pidlSyncSetup => _GetVI(0x0F214138, 0xB1D34a90, 0xBBA927CB, 0xC0C5389A); public static Pidl pidlUsersFiles => _GetVI(0xf3ce0f7c, 0x49014acc, 0x8648d5d4, 0x4b04ef8f); public static Pidl pidlUsersLibraries => _GetVI(0xA302545D, 0xDEFF464b, 0xABE861C8, 0x648D939B); } #endregion #region other paths /// /// Temp folder (temporary files) of this user account. /// public static FolderPath Temp => new(__temp ??= Path.GetTempPath().TrimEnd('\\')); static string __temp; /// /// Folder containing assemblies of this app. /// /// /// Uses . /// /// public static FolderPath ThisApp => new(__thisApp ??= ThisAppBS.TrimEnd('\\')); static string __thisApp; /// /// with appended backslash character. /// /// /// Uses . /// public static string ThisAppBS => __thisAppBS ??= AppContext.BaseDirectory; //info: AppDomain.CurrentDomain.BaseDirectory calls it static string __thisAppBS; //Can change: AppDomain.CurrentDomain.SetData("APP_CONTEXT_BASE_DIRECTORY", "C:\\"); #region set auto/once /// /// Don't auto-create folders when accessing , , or in this thread. /// /// /// Normally this is used temporarily, then restored (set = false). /// public static bool noAutoCreate { get => _noAutoCreate; set { _noAutoCreate = value; } } [ThreadStatic] static bool _noAutoCreate; static readonly object _lock = new(); static string _SetAuto(ref string propVar, string value, bool create) { lock (_lock) { if (propVar == null) { if (create && !_noAutoCreate) filesystem.createDirectory(value); propVar = value; } } return propVar; } static void _SetOnce(ref string propVar, string value, bool create, [CallerMemberName] string m_ = null) { lock (_lock) { if (propVar != null) { #if DEBUG if (!Debugger.IsAttached) //debugger may get the property. Then _SetAuto sets default value. #endif throw new InvalidOperationException("folders." + m_ + " is already set."); } if (create && !_noAutoCreate) filesystem.createDirectory(value); propVar = value; } } static string _DefThisApp(string baseDir, string portableSubdir, bool warning = false) { bool ee = script.role == SRole.EditorExtension; if (ScriptEditor.IsPortable) return PortableData_ + "\\" + portableSubdir + (ee ? null : @"\_script"); return baseDir + (ee ? @"\LibreAutomate" : @"\LibreAutomate\_script"); } internal static string PortableData_ => __portableData ??= GetPortableDataDir_(Editor.Path); static string __portableData; static internal string GetPortableDataDir_(string appPath) { var s = appPath + @"\data"; //if (filesystem.exists(s, true).File) return pathname.normalize(filesystem.loadText(s), appPath); return s; } #endregion /// /// Gets or sets path of folder "temporary files of this application". /// /// Thrown by the set function if this property is already set. /// /// Default path depends on script role ( and portable mode (): /// - miniProgram and exeProgram - folders.Temp + @"LibreAutomate\_script". Or folders.Temp + "Au", if exists (for backward compatibility). /// - editorExtension - folders.Temp + "LibreAutomate". Cannot be changed. /// - Portable miniProgram and exeProgram - folders.ThisApp + @"data\temp\_script". Note: exeProgram launched not from editor isn't portable. /// - Portable editorExtension - folders.ThisApp + @"data\temp". Cannot be changed. /// /// The set function does not change system settings. It just remembers a string that will be later returned by the get function in this process. /// Creates the folder if does not exist when set or get function called first time in this process, unless true. /// public static FolderPath ThisAppTemp { get => new(__thisAppTemp ?? _SetAuto(ref __thisAppTemp, _DefThisApp(Temp, "temp"), create: true)); set => _SetOnce(ref __thisAppTemp, value, create: true); } static string __thisAppTemp; /// /// Gets or sets path of folder "user document files of this application". /// /// Thrown by the set function if this property is already set. /// /// Default path depends on script role () and portable mode (): /// - miniProgram and exeProgram - folders.Documents + @"LibreAutomate\_script". Or folders.Documents + "Au", if exists (for backward compatibility). /// - editorExtension - folders.Documents + "LibreAutomate". Cannot be changed. /// - Portable miniProgram and exeProgram - folders.ThisApp + @"data\doc\_script". Note: exeProgram launched not from editor isn't portable. /// - Portable editorExtension - folders.ThisApp + @"data\doc". Cannot be changed. /// /// The set function does not change system settings. It just remembers a string that will be later returned by the get function in this process. /// Creates the folder if does not exist when set or get function called first time in this process, unless true. /// public static FolderPath ThisAppDocuments { get => new(__thisAppDocuments ?? _SetAuto(ref __thisAppDocuments, _DefThisApp(Documents, "doc", warning: true), create: true)); set => _SetOnce(ref __thisAppDocuments, value, create: true); } static string __thisAppDocuments; /// /// Gets or sets path of folder "local (non-roaming) private files of this application of this user account". /// /// Thrown by the set function if this property is already set. /// /// Default path depends on script role ( and portable mode (): /// - miniProgram and exeProgram - folders.LocalAppData + @"LibreAutomate\_script". Or folders.LocalAppData + "Au", if exists (for backward compatibility). /// - editorExtension - folders.LocalAppData + "LibreAutomate". Cannot be changed. /// - Portable miniProgram and exeProgram - folders.ThisApp + @"data\appLocal\_script". Note: exeProgram launched not from editor isn't portable. /// - Portable editorExtension - folders.ThisApp + @"data\appLocal". Cannot be changed. /// /// The set function does not change system settings. It just remembers a string that will be later returned by the get function in this process. /// Creates the folder if does not exist when set or get function called first time in this process, unless true. /// public static FolderPath ThisAppDataLocal { get => new(__thisAppDataLocal ?? _SetAuto(ref __thisAppDataLocal, _DefThisApp(LocalAppData, "appLocal", warning: true), create: true)); set => _SetOnce(ref __thisAppDataLocal, value, create: true); } static string __thisAppDataLocal; /// /// Gets or sets path of folder "roaming private files of this application of this user account". /// /// Thrown by the set function if this property is already set. /// /// Default path depends on script role ( and portable mode (): /// - miniProgram and exeProgram - folders.RoamingAppData + @"LibreAutomate\_script". Or folders.RoamingAppData + "Au", if exists (for backward compatibility). /// - editorExtension - folders.RoamingAppData + "LibreAutomate". Cannot be changed. /// - Portable miniProgram and exeProgram - folders.ThisApp + @"data\appRoaming\_script". Note: exeProgram launched not from editor isn't portable. /// - Portable editorExtension - folders.ThisApp + @"data\appRoaming". Cannot be changed. /// /// The set function does not change system settings. It just remembers a string that will be later returned by the get function in this process. /// Creates the folder if does not exist when set or get function called first time in this process, unless true. /// public static FolderPath ThisAppDataRoaming { get => new(__thisAppDataRoaming ?? _SetAuto(ref __thisAppDataRoaming, _DefThisApp(RoamingAppData, "appRoaming"), create: true)); set => _SetOnce(ref __thisAppDataRoaming, value, create: true); } static string __thisAppDataRoaming; [EditorBrowsable(EditorBrowsableState.Never)] //renamed [Obsolete("This is an alias for ThisAppDataRoaming.")] public static FolderPath ThisAppData { get => ThisAppDataRoaming; set => ThisAppDataRoaming = value; } //info: unexpandPath ignores ThisAppX. /// /// Gets or sets path of folder "common (all users) private files of this application". /// /// Thrown by the set function if this property is already set. /// /// Default path depends on script role ( and portable mode (): /// - miniProgram and exeProgram - folders.ProgramData + @"LibreAutomate\_script". Or folders.ProgramData + "Au", if exists (for backward compatibility). /// - editorExtension - folders.ProgramData + "LibreAutomate". Cannot be changed. /// - Portable miniProgram and exeProgram - folders.ThisApp + @"data\appCommon\_script". Note: exeProgram launched not from editor isn't portable. /// - Portable editorExtension - folders.ThisApp + @"data\appCommon". Cannot be changed. /// /// The set function does not change system settings. It just remembers a string that will be later returned by the get function in this process. /// This function does not auto-create the folder; usually it is created when installing the application; the script editor does not use and does not install it. /// Note: the ProgramData folder has special security permissions. Programs running not as administrator usually cannot write there, unless your installer changed folder security permissions. /// public static FolderPath ThisAppDataCommon { get => new(__thisAppDataCommon ?? _SetAuto(ref __thisAppDataCommon, _DefThisApp(ProgramData, "appCommon"), create: false)); set => _SetOnce(ref __thisAppDataCommon, value, create: false); } static string __thisAppDataCommon; /// /// Gets or sets path of folder "images of this application". /// /// Thrown by the set function if this property is already set. /// /// Default is ThisAppBS + "Images". /// /// Used by functions of these classes: , , , , possibly some other. /// This function does not auto-create the folder; usually it is created when installing the application; the script editor does not use and does not install it. /// public static FolderPath ThisAppImages { get => new(__thisAppImages ?? _SetAuto(ref __thisAppImages, ThisAppBS + "Images", create: false)); set => _SetOnce(ref __thisAppImages, value, create: false); } static string __thisAppImages; /// /// Gets the root directory of this application, like @"C:\" or @"\\server\share\". /// /// /// See . /// public static string ThisAppDriveBS => __thisAppDrive ??= pathname.GetRootBS_(ThisAppBS); static string __thisAppDrive; //public static FolderPath ThisAppDrive => new(__thisAppDrive ??= Path.GetPathRoot(ThisAppBS)); /// /// Gets folder of the script editor. /// Available in the script editor process and in scripts launched from it. Elsewhere null. /// public static FolderPath Editor { get; internal set; } /// /// Gets folder of current workspace. /// Available in the script editor process and in scripts launched from it. Elsewhere null. /// public static FolderPath Workspace { get => __workspace; internal set { __workspace = value; WorkspaceDriveBS = pathname.GetRootBS_(value); } } static FolderPath __workspace; /// /// Gets the root directory of , like @"C:\" or @"\\server\share\". /// /// /// See . /// public static string WorkspaceDriveBS { get; private set; } //CONSIDER: //public static FolderPath ThisLibrarySettings { get => field ??= ThisAppDataRoaming; set; } #if !DEBUG //fbc /// /// Gets drive type (fixed, removable, network, etc) of . /// [EditorBrowsable(EditorBrowsableState.Never)] //more annoying than useful. Intellisense selects it when the user types "thisApp". And maybe this class isn't the best place for it. Probably users will not look for such function here or somewhere in this library; they'll use DriveInfo. In any case, this is not very useful because: 1. Detects external SSD as Fixed; 2. The removable drive (SSD or not) may be used either as portable or always with the same computer. public static DriveType thisAppDriveType => __driveTypeApp ??= __GetDriveType(ThisAppDriveBS); static DriveType? __driveTypeApp; /// /// Gets drive type (fixed, removable, network, etc) of . /// [EditorBrowsable(EditorBrowsableState.Never)] public static DriveType workspaceDriveType => __GetDriveType(WorkspaceDriveBS); //note: don't use this func with any paths. It is for the above 2 funcs only. static DriveType __GetDriveType(string path) { path = pathname.unprefixLongPath(path); if (path.Starts(@"\\")) return DriveType.Network; //GetDriveType does not support it. DriveInfo throws exception. return (DriveType)Api.GetDriveType(path); } #endif /// /// Gets folder path of the caller source code file. /// /// [](xref:caller_info) /// /// public static FolderPath sourceCode([CallerFilePath] string f_ = null) => new(pathname.getDirectory(f_)); /// /// Gets folder path of the main source code file of this program or of a library. /// /// An assembly compiled by LibreAutomate. If null, uses . /// public static FolderPath sourceCodeMain(Assembly asm = null) => new(pathname.getDirectory(script.sourcePath(false, asm))); /// /// Gets non-redirected path of the System32 folder. /// /// /// If this process is 32-bit and OS is 64-bit, when it uses the folder path (@"C:\WINDOWS\system32"), the OS in most cases redirects it to @"C:\Windows\SysWOW64", which contains 32-bit versions of program files. Use SystemX64 when you want to avoid the redirection and access the true System32 folder which on 64-bit OS contains 64-bit program files. /// More info in class help. /// /// /// public static FolderPath SystemX64 => new(__SystemX64 ??= osVersion.is32BitProcessAnd64BitOS ? Windows + "Sysnative" : System); static string __SystemX64; /// More info in class help. public static FolderPath ProgramFilesX64 => new(__ProgramFilesX64 ??= osVersion.is32BitProcessAnd64BitOS ? envVar("ProgramW6432") : ProgramFiles); static string __ProgramFilesX64; /// More info in class help. public static FolderPath ProgramFilesCommonX64 => new(__ProgramFilesCommonX64 ??= osVersion.is32BitProcessAnd64BitOS ? envVar("CommonProgramW6432") : ProgramFilesCommon); static string __ProgramFilesCommonX64; //The normal retrieving method for these folders is broken. Fails even on 64-bit OS if process is 32-bit. /// /// Gets .NET runtime folder, like C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.6. /// public static FolderPath NetRuntime => new(__netRuntime ??= NetRuntimeBS.TrimEnd('\\')); static string __netRuntime; /// /// Gets .NET runtime folder with '\\' at the end, like C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.6\. /// public static string NetRuntimeBS => __netRuntimeBS ??= RuntimeEnvironment.GetRuntimeDirectory(); static string __netRuntimeBS; /// /// Gets .NET runtime desktop folder, like C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App\8.0.6. /// public static FolderPath NetRuntimeDesktop => new(__netRuntimeDesktop ??= NetRuntimeDesktopBS.TrimEnd('\\')); static string __netRuntimeDesktop; /// /// Gets .NET runtime desktop folder with '\\' at the end, like C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App\8.0.6\. /// public static string NetRuntimeDesktopBS => __netRuntimeDesktopBS ??= _NetRuntimeDesktopBS(); static string __netRuntimeDesktopBS; static string _NetRuntimeDesktopBS() { var s = typeof(Accessibility.IAccessible).Assembly.Location; if (s.NE()) return NetRuntimeBS; //single-file app s = pathname.getDirectory(s, withSeparator: true); Debug.Assert(s != NetRuntimeBS || s.Starts(ThisAppBS)); return s; //note: cannot get NetRuntimeDesktopBS from NetRuntimeBS. // Can be different version, eg Microsoft.NETCore.App\8.0.0-rc.1.23419.4 and Microsoft.WindowsDesktop.App\8.0.0-rc.1.23420.5. } /// /// Gets CD/DVD drive path, like @"D:\". /// /// null if unavailable. public static FolderPath CdDvdDrive { get { foreach (var di in DriveInfo.GetDrives()) { if (di.DriveType == DriveType.CDRom) return new(di.Name); } return default; } } /// Calls (0). public static FolderPath RemovableDrive0 => removableDrive(0); /// Calls (1). public static FolderPath RemovableDrive1 => removableDrive(1); /// Calls (2). public static FolderPath RemovableDrive2 => removableDrive(2); /// Calls (3). public static FolderPath RemovableDrive3 => removableDrive(3); /// /// Gets removable/external/USB drive path, like @"F:\". /// /// null if unavailable. /// 0-based removable drive index. /// /// Calls and counts drives of type . /// public static FolderPath removableDrive(int driveIndex = 0) { foreach (DriveInfo di in DriveInfo.GetDrives()) { if (di.DriveType == DriveType.Removable && driveIndex-- == 0) return new(di.Name); } return default; } /// /// Gets removable/external/USB drive name (like @"F:\") by its volume label. /// /// null if unavailable. /// Volume label. You can see it in drive Properties dialog; it is not the drive name that is displayed in File Explorer. public static FolderPath removableDrive(string volumeLabel) { foreach (DriveInfo di in DriveInfo.GetDrives()) { if (di.DriveType == DriveType.Removable) { string v = null; try { v = di.VolumeLabel; } catch { continue; } if (!v.Eqi(volumeLabel)) continue; return new(di.Name); } } return default; } /// /// Gets the value of an environment variable in current process. /// /// null if variable not found. /// /// /// public static FolderPath envVar(string envVar) => new(Api.GetEnvironmentVariable(envVar)); #endregion #region private functions //Gets non-virtual known folder path from KNOWNFOLDERID specified with 4 uints. static FolderPath _Get(uint a, uint b, uint c, uint d) { //info: we don't use caching. It seems the API use caching internally. //tested: with IKnownFolder much slower. var guid = new _Api.KNOWNFOLDERID(a, b, c, d); if (0 != _Api.SHGetKnownFolderPath(guid, _Api.KNOWN_FOLDER_FLAG.KF_FLAG_DONT_VERIFY, default, out string R)) R = null; return new(R); } //Gets virtual known folder ITEMIDLIST from KNOWNFOLDERID specified with 4 uints. static Pidl _GetVI(uint a, uint b, uint c, uint d) { var guid = new _Api.KNOWNFOLDERID(a, b, c, d); if (0 != _Api.SHGetKnownFolderIDList(guid, _Api.KNOWN_FOLDER_FLAG.KF_FLAG_DONT_VERIFY, default, out IntPtr pidl)) return null; return new Pidl(pidl); } //Gets virtual known folder ITEMIDLIST from KNOWNFOLDERID specified with 4 uints. //Returns string ":: ITEMIDLIST". static FolderPath _GetV(uint a, uint b, uint c, uint d) { using var pidl = _GetVI(a, b, c, d); //never mind: could do it without creating new Pidl return new(pidl?.ToHexString()); } #endregion #region API static class _Api { //GUID that can be inited with 4 uints. internal struct KNOWNFOLDERID { uint _a; ushort _b, _c; byte _d, _e, _f, _g, _h, _i, _j, _k; public KNOWNFOLDERID(uint a, uint b, uint c, uint d) { _a = a; _b = (ushort)(b >> 16); _c = (ushort)b; _d = (byte)(c >> 24); _e = (byte)(c >> 16); _f = (byte)(c >> 8); _g = (byte)c; _h = (byte)(d >> 24); _i = (byte)(d >> 16); _j = (byte)(d >> 8); _k = (byte)d; } } [DllImport("shell32.dll")] internal static extern int SHGetKnownFolderPath(in KNOWNFOLDERID rfid, KNOWN_FOLDER_FLAG dwFlags, IntPtr hToken, out string ppszPath); [DllImport("shell32.dll")] internal static extern int SHGetKnownFolderIDList(in KNOWNFOLDERID rfid, KNOWN_FOLDER_FLAG dwFlags, IntPtr hToken, out IntPtr ppidl); [Flags] internal enum KNOWN_FOLDER_FLAG : uint { KF_FLAG_SIMPLE_IDLIST = 0x00000100, KF_FLAG_NOT_PARENT_RELATIVE = 0x00000200, KF_FLAG_DEFAULT_PATH = 0x00000400, KF_FLAG_INIT = 0x00000800, KF_FLAG_NO_ALIAS = 0x00001000, KF_FLAG_DONT_UNEXPAND = 0x00002000, KF_FLAG_DONT_VERIFY = 0x00004000, KF_FLAG_CREATE = 0x00008000, KF_FLAG_NO_APPCONTAINER_REDIRECTION = 0x00010000, KF_FLAG_ALIAS_ONLY = 0x80000000 } internal enum KF_DEFINITION_FLAGS { KFDF_LOCAL_REDIRECT_ONLY = 0x2, KFDF_ROAMABLE = 0x4, KFDF_PRECREATE = 0x8, KFDF_STREAM = 0x10, KFDF_PUBLISHEXPANDEDPATH = 0x20 } #pragma warning disable CS0649 //field never assigned internal struct KNOWNFOLDER_DEFINITION { public KF_CATEGORY category; public string pszName; public string pszDescription; public Guid fidParent; public string pszRelativePath; public string pszParsingName; public string pszToolTip; public string pszLocalizedName; public string pszIcon; public string pszSecurity; public uint dwAttributes; public KF_DEFINITION_FLAGS kfdFlags; public Guid ftidType; } #pragma warning restore CS0649 //field never assigned internal enum FFFP_MODE { FFFP_EXACTMATCH, FFFP_NEARESTPARENTMATCH } [ComImport, Guid("8BE2D872-86AA-4d47-B776-32CCA40C7018"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal unsafe interface IKnownFolderManager { [PreserveSig] int FolderIdFromCsidl(int nCsidl, out Guid pfid); [PreserveSig] int FolderIdToCsidl(in Guid rfid, out int pnCsidl); [PreserveSig] int GetFolderIds(out Guid* ppKFId, out int pCount); [PreserveSig] int GetFolder(in Guid rfid, out IKnownFolder ppkf); [PreserveSig] int GetFolderByName([In, MarshalAs(UnmanagedType.LPWStr)] string pszCanonicalName, out IKnownFolder ppkf); //[PreserveSig] int RegisterFolder(in Guid rfid, in KNOWNFOLDER_DEFINITION pKFD); //[PreserveSig] int UnregisterFolder(in Guid rfid); //[PreserveSig] int FindFolderFromPath([In, MarshalAs(UnmanagedType.LPWStr)] string pszPath, FFFP_MODE mode, out IKnownFolder ppkf); //[PreserveSig] int FindFolderFromIDList(IntPtr pidl, out IKnownFolder ppkf); //[PreserveSig] int Redirect(in Guid rfid, wnd hwnd, uint flags, [In, MarshalAs(UnmanagedType.LPWStr)] string pszTargetPath, uint cFolders, [MarshalAs(UnmanagedType.LPArray)] [In] Guid[] pExclusion, char** ppszError); } [ComImport, Guid("4df0c730-df9d-4ae3-9153-aa6b82e9795a"), ClassInterface(ClassInterfaceType.None)] internal class KnownFolderManager { } internal enum KF_CATEGORY { KF_CATEGORY_VIRTUAL = 1, KF_CATEGORY_FIXED = 2, KF_CATEGORY_COMMON = 3, KF_CATEGORY_PERUSER = 4 } [ComImport, Guid("3AA7AF7E-9B36-420c-A8E3-F77D4674A488"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal unsafe interface IKnownFolder { [PreserveSig] int GetId(out Guid pkfid); [PreserveSig] int GetCategory(out KF_CATEGORY pCategory); [PreserveSig] int GetShellItem(uint dwFlags, in Guid riid, void** ppv); [PreserveSig] int GetPath(uint dwFlags, [MarshalAs(UnmanagedType.LPWStr)] out string ppszPath); //tested: .NET correctly calls CoTaskMemFree [PreserveSig] int SetPath(uint dwFlags, [In, MarshalAs(UnmanagedType.LPWStr)] string pszPath); [PreserveSig] int GetIDList(uint dwFlags, out IntPtr ppidl); [PreserveSig] int GetFolderType(out Guid pftid); [PreserveSig] int GetRedirectionCapabilities(out uint pCapabilities); [PreserveSig] int GetFolderDefinition(out KNOWNFOLDER_DEFINITION pKFD); } } #endregion #region public methods /// /// Gets canonical names and paths of all known folders, including custom known folders registered by applications. /// These names can be used with . /// /// /// Paths of virtual and unavailable folders are returned as "]]> and "]]>. /// public static unsafe Dictionary getKnownFolders() { var dict = new Dictionary(); var man = new _Api.KnownFolderManager() as _Api.IKnownFolderManager; Guid* gp = null; try { if (man.GetFolderIds(out gp, out int nIds) != 0) return null; for (int i = 0; i < nIds; i++) { _Api.IKnownFolder kf = null; try { if (man.GetFolder(gp[i], out kf) != 0) continue; if (kf.GetFolderDefinition(out var fd) != 0) continue; string path = null; if (fd.category == _Api.KF_CATEGORY.KF_CATEGORY_VIRTUAL) { path = ""; } else { if (kf.GetPath(0, out path) != 0) path = ""; } dict.Add(fd.pszName, path); //tested: .NET correctly frees struct strings. Don't need FreeKnownFolderDefinitionFields, which is an inline function that calls CoTaskMemFree. } catch { } finally { Api.ReleaseComObject(kf); } } } catch { dict = null; } finally { Marshal.FreeCoTaskMem((IntPtr)gp); Api.ReleaseComObject(man); } return dict; } /// /// Gets path of a known folder by its name. /// /// null if unavailable. /// /// Can be: ///
• name of a property of this class, like "Documents", "Temp", "ThisApp". The property must return or string. ///
• name of a property of the nested class , like "shell.ControlPanel". Gets ":: ITEMIDLIST". ///
• known folder canonical name. See . If has prefix "shell.", gets ":: ITEMIDLIST". Much slower, but allows to get paths of folders registered by applications. /// /// public static FolderPath getFolder(string folderName) { if (folderName.NE()) return default; bool isVirtual = folderName.Starts("shell."); if (isVirtual) folderName = folderName[6..]; //properties of this class Type ty = isVirtual ? typeof(shell) : typeof(folders); switch (ty.GetProperty(folderName)?.GetValue(null)) { case FolderPath fp: return fp; case string fp: return new(fp); } //Using reflection is not the fastest way, but simplest, cannot make bugs, and don't need maintenance. Fast enough. //default and custom registered known folders by canonical name string R = null; _Api.IKnownFolderManager man = null; _Api.IKnownFolder kf = null; try { man = (_Api.IKnownFolderManager)new _Api.KnownFolderManager(); if (man.GetFolderByName(folderName, out kf) != 0) return default; if (isVirtual) { if (0 != kf.GetIDList(0, out IntPtr pidl)) return default; R = Pidl.ToHexString(pidl); Marshal.FreeCoTaskMem(pidl); } else { if (0 != kf.GetPath(0, out R)) return default; R = pathname.expand(R); } //tested: works in MTA apartment too. And all props. } catch { } finally { Api.ReleaseComObject(kf); Api.ReleaseComObject(man); } return new(R); //speed: // The get-property code is 2 times slower than calling properties directly. // The IKnownFolderManager code is 90 times slower than the get-property code. } #endregion //DON'T: public static class VirtualNAME that returns parsing name, eg "::{CLSID}\...". // Good: native/.NET shellexecute supports it. // Bad: native/.NET shellexecute supports only some. Almost nothing works in a 32-bit process on 64-bit OS; then even cannot convert the string to ITEMIDLIST. Some parsing names have other formats and the API gets wrong parsing names. //DON'T: The + operator returns FolderPath. Then folders.Desktop + subfolder + file would return "desktop\subfolder\file". Probably not good. //CONSIDER: operator / instead of +. Then could do things like 'folders.x / "subfolder" / "file"' and 'f /= "append"'. /// /// If string starts with a known/special folder path, gets folder name + relative path and returns true. /// For example if string is "C:\Windows\System32\notepad.exe", gets "folders.System" and "notepad.exe". /// /// Any string. Can be null. Case-insensitive. Supports ":: ITEMIDLIST" (see ). /// Receives special folder string like "folders.System". /// Receives filename or relative path in the folder. /// /// Quite slow first time in process, eg 50 ms, because gets all folder paths. Later uses cached paths. /// /// public static bool unexpandPath(string path, out string folder, out string name) { var p = path; folder = name = null; if (!p.NE()) { p = p.Lower(); if (p.Starts(":: ")) { foreach (var v in _upv.Value) { int n = v.path.Length; if (p.Starts(v.path)) { folder = "folders.shell." + v.name; name = path[n..]; return true; } } } else { p = p.Replace('/', '\\'); foreach (var v in _up.Value) { int n = v.path.Length; if (p.Starts(v.path) && (p.Length == n || p[n] == '\\')) { folder = "folders." + v.name; if (p.Length > n) n++; name = path[n..]; return true; } } if (ScriptEditor.IsPortable && path.Starts(ThisAppDriveBS, true)) { folder = "folders.ThisAppDriveBS"; name = path[ThisAppDriveBS.Length..]; return true; } } } return false; } /// /// If string starts with a known/special folder path, replaces that part with %folders.FolderName%. Else returns unchanged string. /// For example if string is "C:\Windows\System32\notepad.exe", returns "%folders.System%\notepad.exe". /// /// Any string. Can be null. Case-insensitive. Supports ":: ITEMIDLIST" (see ). public static string unexpandPath(string path) { if (unexpandPath(path, out var s1, out var s2)) path = s2.NE() ? $@"%{s1}%" : $@"%{s1}%\{s2}"; return path; } static readonly Lazy> _up = new(() => { var a = new List<(string path, string name)>(120); //105 lock (_lock) { var nac = noAutoCreate; noAutoCreate = true; foreach (var pi in typeof(folders).GetProperties()) { if (pi.PropertyType == typeof(FolderPath) && !pi.Name.Starts("ThisApp") //different in processes && pi.GetValue(null) is FolderPath fp) { var s = fp.Path; if (!s.NE()) a.Add((s.Lower(), pi.Name)); } } noAutoCreate = nac; } a.Sort((s1, s2) => { var r = s2.path.Length - s1.path.Length; //prefer longer path, eg C:\A\B to C:\A if (r == 0) { //prefer shorter name, eg System to SystemX64 r = string.CompareOrdinal(s1.path, s2.path); //sorting does not work without this if (r == 0 && s1.path == s2.path) r = s1.name.Length - s2.name.Length; } return r; }); return a; }); static readonly Lazy> _upv = new(() => { var a = new List<(string path, string name)>(30); //22 foreach (var pi in typeof(folders.shell).GetProperties()) { if (pi.PropertyType == typeof(FolderPath) && pi.GetValue(null) is FolderPath fp) { var s = fp.Path; //print.it(pi.Name, s); if (!s.NE()) a.Add((s, pi.Name)); } } return a.OrderByDescending(o => o.path.Length).ToList(); }); /// /// If path starts with one of specified folder paths, replaces that part with the replacements string. Else returns unchanged string. /// /// Any string. Can be null. Case-insensitive. /// Folder paths and replacement strings. /// /// /// public static string unexpandPath(string path, params (string folder, string replacement)[] list) { path = path.Replace('/', '\\'); foreach (var (f_, repl) in list) { var f = f_.Replace('/', '\\'); if (path.Starts(f, true)) { int len = f.Length; if (path.Length == len) return repl; if (path[len] is not ('\\' or '/') && f[^1] is not ('\\' or '/')) continue; return string.Concat(repl, path.AsSpan(len)); } } return path; } } } namespace Au.Types { /// /// Most functions of class return a value of this type. /// Contains folder path (string) and has operator + to append a string with backslash if need. Has implicit conversions from/to string. /// public struct FolderPath { readonly string _path; public FolderPath(string path) { _path = path; } public static explicit operator FolderPath(string path) => new(path); //not implicit. Example: var s = "STRING " + folders.ThisApp; // converts "STRING " to FolderPath and result is @"STRING \C:\path" public static implicit operator string(FolderPath f) => f._path; /// /// Returns . /// public override string ToString() => _path; /// /// Returns the path string. For some folders it can be null. /// public string Path => _path; /// /// Returns the path string. If it is null, throws . /// /// public string PathOrThrow => _path ?? throw new InvalidOperationException("the special folder path is null"); /// /// Returns true if the path is null. /// public bool IsNull => _path == null; /// /// Calls . /// Example: string s = folders.Desktop + "file.txt"; /// /// fp is empty. public static string operator +(FolderPath fp, string append) { if (fp._path.NE()) throw new ArgumentException("No folder path."); return pathname.combine(fp._path, append); } } } ================================================ FILE: Au/Files, data/icon.cs ================================================ using System.Drawing; using System.Reflection.Emit; namespace Au { /// /// Gets icons from/of files etc. Contains native icon handle. /// /// /// Native icons must be destroyed. An icon variable destroys its native icon when disposing. To dispose, call or use using statement. Or use functions like , ; by default they dispose the variable. It's OK to not dispose if you use few icons; GC will do it. /// public class icon : IDisposable { //rejected: base SafeHandle. IntPtr _handle; /// /// Sets native icon handle. /// The icon will be destroyed when disposing this variable or when converting to object of other type. /// public icon(IntPtr hicon) { Debug_.PrintIf(hicon == default, "hicon == default"); //Don't allow to exceed the process handle limit when the program does not dispose them. Default limits are 10000, but min 200. //Icons are USER objects. They also usually create 3 GDI objects (bitmaps?). So a process can have max ~3300 icons by default. if (hicon != default) { GC_.UserHandleCollector.Add(); GC_.GdiHandleCollector.Add(); GC_.GdiHandleCollector.Add(); GC_.GdiHandleCollector.Add(); } //rejected: Add property for native icon ownership or methods for refcounting. // Usually a process uses few icons. If many, the programmer knows the importance of disposing icons; or HandleCollector helps. _handle = hicon; } //FUTURE: FromGdipIcon, FromStream. static icon _New(IntPtr hi) => hi != default ? new(hi) : null; /// /// Destroys native icon handle. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); //never mind: actually this should be in Detach, but then intellisense gives 2 notes } /// protected virtual void Dispose(bool disposing) { var h = Detach(); if (h != default) Api.DestroyIcon(h); } /// ~icon() { Dispose(false); } /// /// Clears this variable and returns its native icon handle. /// public IntPtr Detach() { var h = _handle; if (_handle != default) { _handle = default; GC_.UserHandleCollector.Remove(); GC_.GdiHandleCollector.Remove(); GC_.GdiHandleCollector.Remove(); GC_.GdiHandleCollector.Remove(); } return h; } /// /// Gets native icon handle. /// public IntPtr Handle => _handle; /// /// Gets native icon handle. /// public static implicit operator IntPtr(icon i) => i?._handle ?? default; /// /// Gets icon that can be displayed for a file, folder, shell object, URL or file type. /// /// null if failed. /// /// Can be: ///
• Path of any file or folder. Supports environment variables. If not full path, uses and . ///
• Any shell object, like ":: ITEMIDLIST", @"::{CLSID-1}\::{CLSID-2}", @"shell:AppsFolder\WinStoreAppId". ///
• File type like ".txt", or protocol like "http:". Use "." to get folder icon. ///
• Path with icon resource index or negative id, like "c:\file.dll,4", "c:\file.exe,-4". ///
• URL. /// /// Icon width and height. Default 16. /// /// /// ITEMIDLIST can be of any file, folder, URL or a non-filesystem shell object. See . /// public static icon of(string file, int size = 16, IconGetFlags flags = 0) { using var ds = new _DebugSpeed(file); return _OfFile(file, _NormalizeIconSizeArgument(size), flags); } static icon _OfFile(string file, int size = 16, IconGetFlags flags = 0) { if (file.NE()) return null; file = pathname.expand(file, strict: false); return _GetFileIcon(file, size, flags); } /// /// Gets icon of a file or other shell object specified as ITEMIDLIST. /// /// null if failed. /// ITEMIDLIST. /// Icon width and height. Default 16. public static icon ofPidl(Pidl pidl, int size = 16) { using var ds = new _DebugSpeed(pidl); return _OfPidl(pidl, _NormalizeIconSizeArgument(size)); } static icon _OfPidl(Pidl pidl, int size) { if (pidl?.IsNull ?? true) return null; return _GetShellIcon(true, null, pidl, size); } static icon _GetFileIcon(string file, int size, IconGetFlags flags) { int index = 0; bool extractFromFile = false, isFileType = false, isURL = false, isShellPath = false, isPath = true; //bool getDefaultIfFails = 0!=(flags&IconGetFlags.DefaultIfFails); bool searchPath = 0 == (flags & IconGetFlags.DontSearch); if (0 == (flags & IconGetFlags.LiteralPath)) { //is ".ext" or "protocol:"? isFileType = pathname.IsExtension_(file) || (isURL = pathname.IsProtocol_(file)); if (!isFileType) isURL = pathname.isUrl(file); if (isFileType || isURL || (isShellPath = file[0] == ':')) isPath = false; if (isPath) { //get icon index from "path,index" and remove ",index" extractFromFile = parsePathIndex(file, out file, out index); if (!searchPath) { if (!pathname.isFullPath(file)) file = folders.ThisAppImages + file; file = pathname.Normalize_(file, PNFlags.DontPrefixLongPath, noExpandEV: true); } } } if (isPath) { if (searchPath) { file = filesystem.searchPath(file, folders.ThisAppImages); if (file == null) return null; //ignore getDefaultIfFails } file = pathname.unprefixLongPath(file); } if (isPath /*&& (extractFromFile || 0==(flags&IconGetFlags.Shell))*/) { int ext = 0; if (!extractFromFile && file.Length > 4) ext = file.Ends(true, ".ico", ".exe", ".scr"/*, ".cur", ".ani"*/); if (extractFromFile || ext > 0) { var v = _Load(file, size, index); if (v != null || extractFromFile) return v; switch (filesystem.exists(file, true)) { case 0: return null; case 1: return stock(ext is 2 or 3 ? StockIcon.APPLICATION : StockIcon.DOCNOASSOC, size); //case 2: //folder name ends with .ico etc } } else if (file.Ends(".lnk", true)) { var v = _GetLnkIcon(file, size); if (v != null) return v; //print.it("_GetLnkIcon failed", file); } //note: here we don't cache icons. //Fast enough for where we use this. OS file buffers remain in memory for some time. //Where need, should instead use imagelists or some external cache that saves eg full toolbar bitmap. //SHGetFileInfo has its own cache. In some cases it makes faster (except first time in process), but using it to get all icons is much slower. } bool isExt = isFileType && !isURL; //Can use this code to avoid slow shell API if possible. //In some test cases can make ~2 times faster (with thread pool), especially in MTA thread. //But now, after other optimizations applied, in real life makes faster just 10-20%. #if false //if(0==(flags&IconGetFlags.Shell)){ string progId = isShellPath ? null : filesystem.more.getFileTypeOrProtocolRegistryKey(file, isFileType, isURL); RegistryKey rk = (progId == null) ? null : ARegistry.Open(progId, Registry.ClassesRoot); //print.it(file, progId, isFileType, isURL, rk != null); if(rk == null) { //Unregistered file type/protocol, no extension, folder, ::{CLSID}, shell:AppsFolder\WinStoreAppId, or no progId key in HKCR //print.it(@"unregistered", file, progId); if(progId != null) goto gr; //the file type is known, but no progid key in HKCR. Let shell API figure out. Rare. if(isExt || (isPath && filesystem.exists(file).File)) return Stock(StockIcon.DOCNOASSOC, size); goto gr; } //Registered file type/protocol. using(rk) { if(ARegistry.KeyExists(@"ShellEx\IconHandler", rk)) { //print.it(@"handler", file); goto gr; } string si; if(ARegistry.GetString(out si, "", @"DefaultIcon", rk) && si.Length > 0) { //print.it("registry: DefaultIcon", file, si); if(si[0] == '@') si = null; //eg @{Microsoft.Windows.Photos_16.622.13140.0_x64__8wekyb3d8bbwe?ms-resource://Microsoft.Windows.Photos/Files/Assets/PhotosLogoExtensions.png} else ParseIconLocation(ref si, out index); } else if(ARegistry.GetString(out si, "", @"shell\open\command", rk) && si.Length > 0) { //print.it(@"registry: shell\open\command", file, si); var a = si.SegSplit((si[0] == '"') ? "\"" : " ", StringSplitOptions.RemoveEmptyEntries); si = (a.Length == 0) ? null : a[0]; if(si.Ends("rundll32.exe", true)) si = null; } else { si = null; //print.it("registry: no", file); //Usually shell API somehow gets si. //For example also looks in .ext > PerceivedType > HKCR\SystemFileAssociations. //We can use AssocQueryString(ASSOCSTR_DEFAULTICON), but it is slow and not always gets correct si. } //if(si != null) print.it(file, si); if(si == "%1") { //print.it(file); if(isPath) si = file; else si = null; } if(si != null) { si = pathname.expand(si); if(!pathname.isFullPath(si)) si = folders.System + si; vat v = _Load(si, size, index); if(v != null) return v; } } //} gr: #endif return _GetShellIcon(!isExt, file, null, size); } //usePidl - if pidl not null/IsNull, use pidl, else convert file to PIDL. If false, pidl must be null. static icon _GetShellIcon(bool usePidl, string file, Pidl pidl, int size, bool freePidl = false) { //info: // We support everything that can have icon - path, URL, protocol (eg "http:"), file extension (eg ".txt"), shell item parsing name (eg "::{CLSID}"), "shell:AppsFolder\WinStoreAppId". // We call PidlFromString here and pass it to SHGetFileInfo. It makes faster when using thread pool, because multiple threads can call PidlFromString (slow) simultaneously. // PidlFromString does not support file extension. SHGetFileInfo does not support URL and protocol, unless used PIDL. // SHGetFileInfo gets the most correct icons, but only of standard sizes, which also depends on DPI and don't know what. // IExtractIcon for some types fails or gets wrong icon. Even cannot use it to get correct-size icons, because for most file types it uses system imagelists, which are DPI-dependent. // SHMapPIDLToSystemImageListIndex+SHGetImageList also is not better. //FUTURE: make faster "shell:...". Now eg 100 ms for Settings first time (50 ms Pidl.FromString_ and 50 ms SHGetFileInfo). // Alternatives (not tested): https://stackoverflow.com/questions/32122679/getting-icon-of-modern-windows-app-from-a-desktop-application // Not a big problem when using caching. // Also some icons have incorrect background, eg Calculator. var pidl2 = pidl?.UnsafePtr ?? default; if (usePidl) { if (pidl2 == default) { pidl2 = Pidl.FromString_(file); if (pidl2 == default) usePidl = false; else freePidl = true; } } if (!usePidl) { Debug.Assert(pidl2 == default && file != null); pidl2 = Marshal.StringToCoTaskMemUni(file); freePidl = true; } if (pidl2 == default) return null; //This is faster but fails with some files etc, randomly with others. //It means that shell API and/or extensions are not thread-safe, even if can run in MTA. //return _GetShellIcon2(pidl2, size, usePidl); icon R; try { if (Thread.CurrentThread.GetApartmentState() == ApartmentState.STA) { R = _GetShellIcon2(usePidl, pidl2, size); } else { //tested: switching thread does not make slower. The speed depends mostly on locking, because then thread pool threads must wait. #if true R = Task.Factory.StartNew(() => _GetShellIcon2(usePidl, pidl2, size), default, 0, StaTaskScheduler_.Default).Result_(); #else //old code, uses ThreadPoolSTA_ using var work = ThreadPoolSTA_.CreateWork(null, o => { R = _GetShellIcon2(usePidl, pidl2, size); }); work.Submit(); work.Wait(); #endif } } finally { if (freePidl) Marshal.FreeCoTaskMem(pidl2); } GC.KeepAlive(pidl); return R; } static icon _GetShellIcon2(bool isPidl, IntPtr pidl, int size) { IntPtr il = default; int index = -1, ilIndex, realSize; if (size < (realSize = 16) * 5 / 4) ilIndex = Api.SHIL_SMALL; else if (size < (realSize = 32) * 5 / 4) ilIndex = Api.SHIL_LARGE; else if (size < 256) { ilIndex = Api.SHIL_EXTRALARGE; realSize = 48; //info: cannot resize from 256 because GetIcon(SHIL_JUMBO) gives 48 icon if 256 icon unavailable. Getting real icon size is either impossible or quite difficult and slow (not tested). } else { ilIndex = Api.SHIL_JUMBO; realSize = 256; } using var dac = new Dpi.AwarenessContext(-1); //unaware //Need to lock this part, or randomly fails with some file types. lock ("TK6Z4XiSxkGSfC14/or5Mw") { try { uint fl = Api.SHGFI_SYSICONINDEX | Api.SHGFI_SHELLICONSIZE; if (ilIndex == Api.SHIL_SMALL) fl |= Api.SHGFI_SMALLICON; if (isPidl) fl |= Api.SHGFI_PIDL; else fl |= Api.SHGFI_USEFILEATTRIBUTES; il = Api.SHGetFileInfo(pidl, out var x, fl); if (il != default) index = x.iIcon; //Marshal.Release(il); //undocumented, but without it IImageList refcount grows. Probably it's ok, because it is static, never deleted until process exits. } catch { Debug_.Print("exception"); } //Shell extensions may throw. //By default .NET does not allow to handle eg access violation exceptions. // Previously we would add [HandleProcessCorruptedStateExceptions], but Core ignores it. // Now our AppHost sets environment variable COMPlus_legacyCorruptedStateExceptionsPolicy=1 before loading runtime. // Or could move the API call to the C++ dll. } if (index < 0) return null; //note: Getting icon from imagelist must be in STA thread too, else fails with some file types. //tested: This part works without locking. Using another lock here makes slower. try { if (ilIndex == Api.SHIL_SMALL || ilIndex == Api.SHIL_LARGE || _GetShellImageList(ilIndex, out il)) { //print.it(il, Debug_.GetComObjRefCount(il)); var hi = Api.ImageList_GetIcon(il, index, 0); if (hi != default) { if (size != realSize) { //print.it(size, realSize, index, file); hi = Api.CopyImage(hi, Api.IMAGE_ICON, size, size, Api.LR_COPYDELETEORG | Api.LR_COPYRETURNORG); //never mind if fails, it's too rare. //TODO3: test LR_COPYFROMRESOURCE. } return _New(hi); } } } catch (Exception e) { Debug_.Print(e); } //finally { if(il != default) Marshal.Release(il); } return null; static bool _GetShellImageList(int ilIndex, out IntPtr R) { lock ("vK6Z4XiSxkGSfC14/or5Mw") { //the API fails if called simultaneously by multiple threads if (0 == Api.SHGetImageList(ilIndex, Api.IID_IImageList, out R)) return true; } Debug.Assert(false); return false; } } //Gets shortcut (.lnk) icon. //Much faster than other shell API. //Also gets correct icon where iextracticon fails and/or shgetfileinfo gets blank document icon, don't know why. //Usually fails only when target does not exist. Then iextracticon also fails, and shgetfileinfo gets blank document icon. //If fails, returns null. No exceptions. static icon _GetLnkIcon(string file, int size) { try { using var x = shortcutFile.open(file); var s = x.GetIconLocation(out int ii); if (s != null) return _Load(s, size, ii); s = x.TargetPathRawMSI; if (s != null) return _OfFile(s, size, IconGetFlags.DontSearch); //print.it("need IDList", file); using (var pidl = x.TargetPidl) return _OfPidl(pidl, size); } catch { return null; } } /// /// Extracts icon directly from file. /// /// null if failed. /// .ico, .exe, .dll or other file that contains one or more icons. Also supports cursor files - .cur, .ani. Must be full path, without icon index. Supports environment variables (see ). /// Icon width and height. Default 16. /// Icon index or negative icon resource id in the .exe/.dll file. public static icon load(string file, int size = 16, int index = 0) { using var ds = new _DebugSpeed(file); return _Load(file, _NormalizeIconSizeArgument(size), index); } static unsafe icon _Load(string file, int size, int index) { //We use SHDefExtractIcon because of better quality resizing (although several times slower) which matches shell and imagelist resizing. //With .ico it matches LoadImage speed (without resizing). PrivateExtractIcons is slightly slower. if (file.NE()) return null; IntPtr hi = default; int hr = Api.SHDefExtractIcon(file, index, 0, &hi, null, size); return hr == 0 ? _New(hi) : null; //if(Api.PrivateExtractIcons(file, index, size, size, out R, default, 1, 0) != 1) return null; //SHOULDOO: test LoadIconWithScaleDown. If .ico or negative index. But maybe SHDefExtractIcon uses it, that is why better quality. } /// /// Gets a shell stock icon. /// /// null if failed. /// Shell stock icon id. /// Icon width and height. Default 16. public static unsafe icon stock(StockIcon id, int size = 16) { if (!GetStockIconLocation_(id, out var path, out int index)) return null; return load(path, size, index); //note: don't cache, because of the quota of handles a process can have. Maybe only exe and document icons; maybe also folder and open folder. //tested: always gets 32x32 icon: Api.LoadImage(default, 32516, Api.IMAGE_ICON, 16, 16, Api.LR_SHARED); //OIC_INFORMATION } internal static unsafe bool GetStockIconLocation_(StockIcon id, out string path, out int index) { var x = new Api.SHSTOCKICONINFO(); x.cbSize = Api.SizeOf(x); if (0 == Api.SHGetStockIconInfo(id, 0, ref x)) { path = new string(x.szPath); index = x.iIcon; return true; } else { path = null; index = 0; return false; } } /// /// Gets icon from unmanaged resources of this program. /// /// null if not found. /// Icon width and height. Default 16. /// Native resource id. Default IDI_APPLICATION (C# compilers add app icon with this id). /// /// If role miniProgram (default), at first looks in main assembly (.dll); if not found there, looks in .exe file. Else only in .exe file. /// /// The icon is cached and protected from destroying. Don't need to destroy it, and not error to do it. /// public static icon ofThisApp(int size = 16, int resourceId = Api.IDI_APPLICATION) { var hm = GetAppIconModuleHandle_(resourceId); return FromModuleHandle_(hm, size, resourceId); //This is not 100% reliable because the icon id 32512 (IDI_APPLICATION) is undocumented. //I could not find a .NET method to get icon directly from native resources of assembly. //Could use the resource emumeration API... //info: MSDN lies that with LR_SHARED gets a cached icon regardless of size argument. Caches each size separately. Tested on Win 10, 7, XP. //TEST: LoadIconWithScaleDown. } internal static icon FromModuleHandle_(IntPtr hm, int size = 16, int resourceId = Api.IDI_APPLICATION) { if (hm == default) return null; size = _NormalizeIconSizeArgument(size); return _New(Api.LoadImage(hm, resourceId, Api.IMAGE_ICON, size, size, Api.LR_SHARED)); } /// /// Gets icon of tray icon size from unmanaged resources of this program or system. /// /// Native resource id. Default IDI_APPLICATION (C# compilers add app icon with this id). /// /// Calls API LoadIconMetric. /// /// The icon can be in main assembly (if role miniProgram) or in the program file (.exe). If not found, loads standard icon, see API LoadIconMetric. /// public static icon trayIcon(int resourceId = Api.IDI_APPLICATION/*, bool big = false*/) { #if true IntPtr hi = default; int hr = 1; if (script.role == SRole.MiniProgram) hr = Api.LoadIconMetric(Api.GetModuleHandle(AssemblyUtil_.GetEntryAssembly().Location), resourceId, 0, out hi); if (hr != 0) hr = Api.LoadIconMetric(Api.GetModuleHandle(null), resourceId, 0, out hi); if (hr != 0) hr = Api.LoadIconMetric(default, resourceId, 0, out hi); return hr == 0 ? _New(hi) : null; #else //10% slower var h = GetAppIconModuleHandle_(); return 0 == Api.LoadIconMetric(h, resourceId, /*big ? 1 :*/ 0, out var hi) ? new(hi) : null; #endif //can load big icon too, but not very useful. } /// /// Loads icon of tray icon size from .ico file. /// /// null if not found. /// /// Calls API LoadIconMetric. /// public static unsafe icon trayIcon(string icoFile) { fixed (char* p = icoFile) return 0 == Api.LoadIconMetric(default, (nint)p, 0, out var hi) ? _New(hi) : null; //LoadIconMetric bug: does not load large icon from ico file. } /// /// Gets native module handle of exe or dll that contains specified icon. Returns default if no icon. /// If role miniProgram, at first looks in main assembly (.dll). /// internal static IntPtr GetAppIconModuleHandle_(int resourceId) { if (script.role == SRole.MiniProgram) { var h1 = Api.GetModuleHandle(AssemblyUtil_.GetEntryAssembly().Location); if (default != Api.FindResource(h1, resourceId, Api.RT_GROUP_ICON)) return h1; } var h2 = Api.GetModuleHandle(null); if (default != Api.FindResource(h2, resourceId, Api.RT_GROUP_ICON)) return h2; return default; } /// /// Gets icon that is displayed in window title bar and in taskbar button. /// /// null if failed. /// A top-level window of any process. /// Icon width and height. Default 16. public static icon ofWindow(wnd w, int size = 16) { //int size = Api.GetSystemMetrics(big ? Api.SM_CXICON : Api.SM_CXSMICON); //support Windows Store apps var appId = WndUtil.GetWindowsStoreAppId(w, prependShellAppsFolder: true); if (appId != null) { var v = of(appId, size, IconGetFlags.DontSearch); if (v != null) return v; } bool big = size >= 24; //TODO3: make high-DPI-aware. How? bool ok = w.SendTimeout(2000, out nint R, Api.WM_GETICON, big ? 1 : 0); if (R == 0 && ok) w.SendTimeout(2000, out R, Api.WM_GETICON, big ? 0 : 1); if (R == 0) R = WndUtil.GetClassLong(w, big ? GCL.HICON : GCL.HICONSM); if (R == 0) R = WndUtil.GetClassLong(w, big ? GCL.HICONSM : GCL.HICON); //tested this code with DPI 125%. Small icon of most windows match DPI (20), some 16, some 24. //tested: undocumented API InternalGetWindowIcon does not get icon of winstore app. //Copy, because will DestroyIcon, also it resizes if need. if (R != 0) R = Api.CopyImage(R, Api.IMAGE_ICON, size, size, 0); return _New(R); } /// /// Sends WM_SETICON message. /// /// /// ICON_BIG. public void SetWindowIcon(wnd w, bool big) { w.Send(Api.WM_SETICON, big ? 1 : 0, _handle); } /// /// Creates icon at run time. /// /// /// /// Called to draw icon. If null, the icon will be completely transparent. public static icon create(int width, int height, Action drawCallback = null) { IntPtr hi; if (drawCallback != null) { using var b = new Bitmap(width, height); using var g = Graphics.FromImage(b); g.Clear(Color.Transparent); //optional, new bitmaps are transparent, but it is undocumented, and eg .NET Bitmap.MakeTransparent does it drawCallback(g); hi = b.GetHicon(); } else { int nb = Math2.AlignUp(width, 32) / 8 * height; var aAnd = new byte[nb]; for (int i = 0; i < nb; i++) aAnd[i] = 0xff; var aXor = new byte[nb]; hi = Api.CreateIcon(default, width, height, 1, 1, aAnd, aXor); //speed: ~20 mcs. About 10 times faster than above. Faster than CopyImage etc. } return _New(hi); } /// /// Creates object that shares native icon handle with this object. /// /// null if is default(IntPtr). public Icon ToGdipIcon() { if (_handle == default) return null; var R = Icon.FromHandle(_handle); s_cwt.Add(R, this); return R; } static readonly ConditionalWeakTable s_cwt = new(); /// /// Converts native icon to GDI+ bitmap object. /// /// null if is default(IntPtr) or if fails to convert. /// /// If true (default), destroys the native icon object; also clears this variable and don't need to dispose it. /// If false, later will need to dispose this variable. /// public Bitmap ToGdipBitmap(bool destroyIcon = true) { if (_handle != default) { using var ic = Icon.FromHandle(_handle); try { return ic.ToBitmap(); } catch (Exception e) { print.warning("ToGdipBitmap() failed. " + e.ToString(), -1); } finally { if (destroyIcon) Dispose(); } } return null; //note: don't use Bitmap.FromHicon. It just calls GdipCreateBitmapFromHICON which does not support alpha etc. //FUTURE: look for a faster way. } /// /// Converts native icon to WPF image object. /// /// null if is default(IntPtr) or if fails to convert. /// /// If true (default), destroys the native icon object; also clears this variable and don't need to dispose it. /// If false, later will need to dispose this variable. /// /// /// The image is not suitable for WPF window icon. Instead use or WPF image loading functions. /// public System.Windows.Media.Imaging.BitmapSource ToWpfImage(bool destroyIcon = true) { if (_handle != default) { try { return System.Windows.Interop.Imaging.CreateBitmapSourceFromHIcon(_handle, default, default); } catch (Exception e) { print.warning("ToWpfImage() failed. " + e.ToString(), -1); } finally { if (destroyIcon) Dispose(); } } return null; //why not suitable for WPF window icon: //1. Shadows in alpha area. As a workaround could get icon bits and call BitmapFrame.Create(), but //2. For window need 2 icons - small and big. //See script "HICON to ImageSource". } /// /// Gets icon size. /// /// default(SIZE) if failed. public unsafe SIZE Size { get { if (_handle != default) { using Api.ICONINFO ii = new(_handle); Api.BITMAP b = default; bool hasColors = ii.hbmColor != default; Api.GetObject(hasColors ? ii.hbmColor : ii.hbmMask, sizeof(Api.BITMAP), &b); return new(b.bmWidth, hasColors ? b.bmHeight : b.bmHeight / 2); } return default; } } /// /// Parses icon location string. /// /// true if it includes icon index or resource id. /// /// Icon location. Can be "path,index" or "path,-id" or just path. /// Supports path enclosed in "" like "\"path\",index", and spaces between comma and index like "path, index". /// /// Receives path without index and without "". Can be the same variable as s. /// Receives index/id or 0. public static bool parsePathIndex(string s, out string path, out int index) { path = s; index = 0; if (s.NE()) return false; if (s[0] == '"') path = s = s.Replace("\"", ""); //can be eg "path",index if (s.Length < 3) return false; if (!s[^1].IsAsciiDigit()) return false; int i = s.LastIndexOf(','); if (i < 1) return false; index = s.ToInt(i + 1, out int e); if (e != s.Length) return false; path = s[..i]; return true; //note: API PathParseIconLocation has bugs. Eg splits "path,5moreText". Or from "path, 5" removes ", 5" and returns 0. } static int _NormalizeIconSizeArgument(int size) { if (size == 0) return 16; return (uint)size <= 256 ? size : throw new ArgumentOutOfRangeException("size", "Must be 0 - 256"); } //rejected. Not per-monitor-DPI-aware, etc. Better use 16 etc and then auto-scale. ///// ///// Gets size of small icons displayed in UI. ///// Depends on DPI; 16 when DPI 100%. ///// //public static int sizeSmall => Dpi.OfThisProcess / 6; //eg 96/6=16 ///// ///// Gets size of large icons displayed in UI. ///// Depends on DPI; 32 when DPI 100%. ///// //public static int sizeLarge => Dpi.OfThisProcess / 3; //static int _SizeExtraLarge => Dpi.OfThisProcess / 2; //tested: shell imagelist icon sizes match these. //note: don't use GetSystemMetrics(SM_CXSMICON/SM_CXICON). They are for other purposes, eg window title bar, tray icon. On Win7 they can be different because can be changed in Control Panel. Used by SystemInformation.SmallIconSize etc. /// /// If not 0, "get icon" functions of this class will print (in editor's output) their execution time in milliseconds when it >= this value. /// /// /// Icons are mostly used in toolbars and menus. Getting icons of some files can be slow. For example if antivirus program scans the file. Toolbars and menus that use slow icons may start with a noticeable delay. Use this property to find too slow icons. Then you can replace them with fast icons, for example .ico files. /// public static int debugSpeed { get; set; } struct _DebugSpeed : IDisposable { long _time; object _file; //string or Pidl public _DebugSpeed(object file) { if (debugSpeed > 0) { _file = file; _time = perf.ms; } else { _time = 0; _file = null; } //TODO3: implement global icon cache here. File-based. } public void Dispose() { if (_time != 0) { long t = perf.ms - _time; if (t >= debugSpeed) { //if (_file is Pidl p) _file = p.ToShellString(SIGDN.NORMALDISPLAY); print.it($"icon.debugSpeed: {t} ms, {_file}"); } } } } /// /// Gets icon path from code that contains string like @"c:\windows\system32\notepad.exe" or @"%folders.System%\notepad.exe" or URL/shell. /// Also supports code patterns like folders.System + "notepad.exe" or folders.shell.RecycleBin. /// /// null if no such string/pattern. /// /// The string is .cs filename or relative path, but not full path. internal static string ExtractIconPathFromCode_(MethodInfo mi, out bool cs) { //support code pattern like 'folders.System + "notepad.exe"'. // Opcodes: call(folders.System), ldstr("notepad.exe"), FolderPath.op_Addition. //also code pattern like 'folders.System' or 'folders.shell.RecycleBin'. // Opcodes: call(folders.System), FolderPath.op_Implicit(FolderPath to string). //also code pattern like 'run.itSafe("notepad.exe")'. //print.it(mi.Name); cs = false; var il = mi.GetMethodBody().GetILAsByteArray(); if (il.Length > 100) return null; int i = 0, patternStart = -1; MethodInfo f1 = null; string filename = null, filename2 = null; try { var reader = new ILReader(mi, il); foreach (var instruction in reader.Instructions) { if (++i > 100) break; var op = instruction.Op; //print.it(op); if (op == OpCodes.Nop) { i--; } else if (op == OpCodes.Ldstr) { var s = instruction.Data as string; //print.it(s); //print.it(i, patternStart); if (i == patternStart + 1) filename = s; else { if (pathname.isFullPathExpand(ref s)) return s; //eg run.it(@"%folders.System%\notepad.exe"); if (pathname.IsShellPathOrUrl_(s)) return s; filename = null; patternStart = -1; if (i == 1) filename2 = s; } } else if (op == OpCodes.Call && instruction.Data is MethodInfo f && f.IsStatic) { //print.it(f, f.DeclaringType, f.Name, f.MemberType, f.ReturnType, f.GetParameters().Length); var dt = f.DeclaringType; if (dt == typeof(folders) || dt == typeof(folders.shell)) { if (f.ReturnType == typeof(FolderPath) && f.GetParameters().Length == 0) { //print.it(1); f1 = f; patternStart = i; } } else if (dt == typeof(FolderPath)) { if (i == patternStart + 2 && f.Name == "op_Addition") { //print.it(2); var fp = (FolderPath)f1.Invoke(null, null); if (fp.Path == null) return null; return fp + filename; } else if (i == patternStart + 1 && f.Name == "op_Implicit" && f.ReturnType == typeof(string)) { //print.it(3); return (FolderPath)f1.Invoke(null, null); } //} else if (dt == typeof(script)) { // print.it(filename); } } } if (filename2 != null) { if (filename2.Ends(".exe", true)) return filesystem.searchPath(filename2); if (cs = filename2.Ends(".cs", true) && !pathname.isFullPath(filename2, orEnvVar: true)) return filename2; } } catch (Exception ex) { Debug_.Print(ex); } return null; } /// /// Gets image of a Windows Store App. /// /// Bitmap object, or null if failed. Its size may be != size; let the caller scale it when drawing. /// String like @"shell:AppsFolder\Microsoft.WindowsCalculator_8wekyb3d8bbwe!App". /// Desired width and height. public static Bitmap winStoreAppImage(string shellString, int size = 16) { using var idl = Pidl.FromString(shellString); if (idl == null) return null; //the slowest part, > 90% return winStoreAppImage(idl, size); } /// /// Gets image of a Windows Store App. This overload accepts a instead of a shell string. /// /// Bitmap object, or null if failed. Its size may be != size; let the caller scale it when drawing. public static Bitmap winStoreAppImage(Pidl pidl, int size = 16) { var path = _GetWinStoreAppImagePath(pidl, size); if (path == null) return null; //var r = Image.FromFile(path) as Bitmap; //no, locks file using var stream = File.OpenRead(path); var r = Image.FromStream(stream) as Bitmap; r?.SetResolution(96, 96); return r; } static string _GetWinStoreAppImagePath(Pidl pidl, int size = 16) { if (0 != Api.SHBindToParent(pidl.UnsafePtr, typeof(Api.IShellFolder).GUID, out var isf, out var idItem)) return null; if (!isf.GetUIObjectOf(idItem, out Api.IExtractIcon extract)) return null; var sb = new StringBuilder(1000); if (0 != extract.GetIconLocation(0, sb, sb.Capacity, out int index, out uint rflags)) return null; var loc = sb.ToString(); //print.it(loc); if (loc.Ends(".png")) return loc; //Win 8.0, 8.1 Debug_.PrintIf(!loc.Ends(".png-100"), loc); int ipng = loc.LastIndexOf(".png", StringComparison.OrdinalIgnoreCase); if (ipng < 0) return null; loc = loc[..ipng]; if (!_GetPngPathFromIconLoc(out string path)) return null; return path; bool _GetPngPathFromIconLoc(out string path) { path = null; var dir = pathname.getDirectory(loc); if (!filesystem.exists(dir).Directory) return false; var a1 = Directory.GetFiles(dir, pathname.getName(loc) + "*.png"); int nTargetsize = 0, nScale = 0; for (int i = 0; i < a1.Length; i++) { var v = a1[i]; if (v.Find("contrast-", loc.Length, true) >= 0) a1[i] = null; else if (v.Find("targetsize-", loc.Length, true) >= 0) nTargetsize++; else if (v.Find("scale-", loc.Length, true) >= 0) nScale++; } if (nTargetsize == 0 && nScale == 0) { path = a1.FirstOrDefault(o => o.Eqi(loc)); loc += ".png"; return path != null; } var a = new (string path, string q, int size)[nTargetsize > 0 ? nTargetsize : nScale]; int j = 0; foreach (var v in a1) { if (v == null) continue; int i = v.Find(nTargetsize > 0 ? "targetsize-" : "scale-", loc.Length, true); if (i < 0) continue; int z = v.ToInt(i + (nTargetsize > 0 ? 11 : 6)); if (nTargetsize == 0) z = Math2.MulDiv(z, 16, 100); //assume eg scale-100 == targetsize-16 a[j++] = (v, v[loc.Length..^4], z); } //foreach (var k in a) print.it("\t" + k.q, k.size); int bestSize = int.MaxValue, maxSmallerSize = 0; foreach (var v in a) if (v.size == size) { bestSize = size; break; } else if (v.size > size) bestSize = Math.Min(bestSize, v.size); else maxSmallerSize = Math.Max(maxSmallerSize, v.size); if (bestSize == int.MaxValue) bestSize = maxSmallerSize; //print.it(bestSize); foreach (var v in a) if (v.size == bestSize && v.q.Find("altform-lightunplated", true) >= 0) { path = v.path; return true; } //foreach (var v in a) if(v.size==bestSize && v.q.Find("altform-unplated", true)>=0 && v.q.Find("theme-light", true)>=0) { path=v.path; return true; } //foreach (var v in a) if(v.size==bestSize && v.q.Find("theme-light", true)>=0) { path=v.path; return true; } foreach (var v in a) if (v.size == bestSize && v.q.Find("altform-unplated", true) >= 0) { path = v.path; return true; } foreach (var v in a) if (v.size == bestSize) { path = v.path; return true; } return false; } } } } namespace Au.Types { /// /// Flags for and similar functions. /// [Flags] public enum IconGetFlags { /// /// The file argument is literal full path. Don't parse "path,index", don't support ".ext" (file type icon), don't make fully-qualified, etc. /// LiteralPath = 1, /// /// Don't call . /// DontSearch = 2, #if false /// Use shell API for all file types, including exe and ico. Shell=8, //rejected because SHGetFileInfo does not get exe icon with shield overlay /// /// If file does not exist or fails to get its icon, get common icon for that file type, or default document icon if cannot get common icon. /// DefaultIfFails = 16, //rejected. Now for exe/ico/etc is like with shell API: if file exists, gets default icon (exe or document), else returns default(IntPtr). #endif } #pragma warning disable 1591 //missing XML documentation /// See , SHSTOCKICONID. public enum StockIcon { DOCNOASSOC, DOCASSOC, APPLICATION, FOLDER, FOLDEROPEN, DRIVE525, DRIVE35, DRIVEREMOVE, DRIVEFIXED, DRIVENET, DRIVENETDISABLED, DRIVECD, DRIVERAM, WORLD, SERVER = 15, PRINTER, MYNETWORK, FIND = 22, HELP, SHARE = 28, LINK, SLOWFILE, RECYCLER, RECYCLERFULL, MEDIACDAUDIO = 40, LOCK = 47, AUTOLIST = 49, PRINTERNET, SERVERSHARE, PRINTERFAX, PRINTERFAXNET, PRINTERFILE, STACK, MEDIASVCD, STUFFEDFOLDER, DRIVEUNKNOWN, DRIVEDVD, MEDIADVD, MEDIADVDRAM, MEDIADVDRW, MEDIADVDR, MEDIADVDROM, MEDIACDAUDIOPLUS, MEDIACDRW, MEDIACDR, MEDIACDBURN, MEDIABLANKCD, MEDIACDROM, AUDIOFILES, IMAGEFILES, VIDEOFILES, MIXEDFILES, FOLDERBACK, FOLDERFRONT, SHIELD, WARNING, INFO, ERROR, KEY, SOFTWARE, RENAME, DELETE, MEDIAAUDIODVD, MEDIAMOVIEDVD, MEDIAENHANCEDCD, MEDIAENHANCEDDVD, MEDIAHDDVD, MEDIABLURAY, MEDIAVCD, MEDIADVDPLUSR, MEDIADVDPLUSRW, DESKTOPPC, MOBILEPC, USERS, MEDIASMARTMEDIA, MEDIACOMPACTFLASH, DEVICECELLPHONE, DEVICECAMERA, DEVICEVIDEOCAMERA, DEVICEAUDIOPLAYER, NETWORKCONNECT, INTERNET, ZIPFILE, SETTINGS, DRIVEHDDVD = 132, DRIVEBD, MEDIAHDDVDROM, MEDIAHDDVDR, MEDIAHDDVDRAM, MEDIABDROM, MEDIABDR, MEDIABDRE, CLUSTEREDDRIVE, MAX_ICONS = 181 } #pragma warning restore 1591 } ================================================ FILE: Au/Files, data/pathname.cs ================================================ //tested: System.IO.Path functions improved in Core. // No exceptions if path contains invalid characters. Although the exceptions are still documented in MSDN. // Support long paths and file streams. // Faster, etc. namespace Au { /// /// File path string functions. Parse, combine, make full, make unique, make valid, expand variables, etc. /// /// /// Most functions of this class work with strings and don't access the file system. Several functions query file system info. /// /// Functions of this class don't throw exceptions when path is invalid (path format, invalid characters). Only throws exception if not full path. /// /// Also you can use .NET class . In its documentation you'll find more info about paths. /// public static unsafe class pathname { //BAD: why pathname? Better would be eg filepath. /// /// If path starts with "%" or "\"%", expands environment variables enclosed in %, else just returns path. /// Also supports known folder names, like "%folders.Documents%". More info in Remarks. /// /// Any string. Can be null. /// /// What to do if path looks like starts with and environment variable or known folder but the variable/folder does not exist: ///
true - throw ; ///
false - return unexpanded path; ///
null (default) - call and return unexpanded path. /// /// /// Supports known folder names. See . /// Example: @"%folders.Documents%\file.txt". /// Example: @"%folders.shell.ControlPanel%" //gets ":: ITEMIDLIST". /// Usually known folders are used like string path = folders.Documents + "file.txt". However it cannot be used when you want to store paths in text files, registry, etc. Then this feature is useful. /// To get known folder path, this function calls . /// /// This function is called by many functions of classes , , , some others, therefore all they support environment variables and known folders in path string. /// /// /// /// public static string expand(string path, bool? strict = null) { var s = path; if (s.Lenn() < 3) return s; if (s[0] != '%') { if (s[0] == '"' && s[1] == '%') return "\"" + expand(s[1..], strict); return s; } int i = s.IndexOf('%', 2); if (i < 0) return s; //return Environment.ExpandEnvironmentVariables(s); //5 times slower //support known folders, like @"%folders.Documents%\...". // rejected: without "folders", like @"%%.Documents%\...". If need really short, can set and use environment variables. //if ((i > 10 && s.Starts("%folders.")) || (i > 4 && s.Starts("%%"))) { // var prop = s[(s[1] == '%' ? 2 : 10)..i]; if (i > 10 && s.Starts("%folders.")) { var prop = s[9..i]; var k = folders.getFolder(prop); if (k != null) { s = s[++i..]; string ks = k.Path; if (ks.Starts(":: ")) return ks + s; //don't need \ return k + s; //add \ if need } //throw new AuException("folders does not have property " + prop); } if (!Api.ExpandEnvironmentStrings(s, out s)) { var err = "Failed to expand path: " + s; if (strict == true) throw new ArgumentException(err); if (strict != false) print.warning(err); return s; } return expand(s, strict); //can be %envVar2% in envVar1 value } /// /// Returns true if the string is full path, like @"C:\a\b.txt" or @"C:" or @"\\server\share\...": /// /// Any string. Can be null. /// Also return true if starts with "%environmentVariable%" or "%folders.Folder%". Note: this function does not check whether the variable/folder exists; for it use instead. /// /// Returns true if path matches one of these wildcard patterns: /// - @"?:\*" - local path, like @"C:\a\b.txt". Here ? is A-Z, a-z. /// - @"?:" - drive name, like @"C:". Here ? is A-Z, a-z. /// - @"\\*" - network path, like @"\\server\share\...". Or has prefix @"\\?\". /// /// Supports '/' characters too. /// /// Supports only file-system paths. Returns false if path is URL () or starts with "::". /// public static bool isFullPath(RStr path, bool orEnvVar = false) { int len = path.Length; if (len >= 2) { if (path[1] == ':' && path[0].IsAsciiAlpha()) { return len == 2 || IsSepChar_(path[2]); //info: returns false if eg "c:abc" which means "abc" in current directory of drive "c:" } switch (path[0]) { case '\\' or '/': return IsSepChar_(path[1]); case '%' when orEnvVar: return path[1..].IndexOf('%') > 1; } } return false; } /// /// Expands environment variables and calls/returns . /// /// true if the string is full path, like @"C:\a\b.txt" or @"C:" or @"\\server\share\...". /// /// Any string. Can be null. /// If starts with '%' character, calls and then with expanded environment variables. If it returns true, replaces the passed variable with the expanded path string. /// /// /// /// Returns true if path matches one of these wildcard patterns: /// - @"?:\*" - local path, like @"C:\a\b.txt". Here ? is A-Z, a-z. /// - @"?:" - drive name, like @"C:". Here ? is A-Z, a-z. /// - @"\\*" - network path, like @"\\server\share\...". Or has prefix @"\\?\". /// /// Supports '/' characters too. /// Supports only file-system paths. Returns false if path is URL () or starts with "::". /// public static bool isFullPathExpand(ref string path, bool? strict = null) => isFullPathExpand(path, out path, strict); /// /// Any string. Can be null. /// If starts with '%' character, calls and then with expanded environment variables. If it returns true, sets path2 = the expanded path string. /// /// Receives the final path. /// public static bool isFullPathExpand(string path, out string path2, bool? strict = null) { var s = path2 = path; if (s.Lenn() < 2) return false; if (s[0] != '%') return isFullPath(s); s = expand(s, strict); if (s[0] == '%') return false; if (!isFullPath(s)) return false; path2 = s; return true; } /// /// Gets the length of the drive or network folder part in path, like @"C:\", @"\\server\share\", @"\\?\C:\", @"\\?\UNC\server\share\", etc. /// /// Full path or any string. Can be null. Should not be @"%environmentVariable%\...". /// /// See . /// public static int getRootLength(RStr path) { int i = Path.GetPathRoot(path).Length; //Span, no alloc if (i > 0 && i < path.Length && !IsSepChar_(path[i - 1]) && IsSepChar_(path[i])) i++; //@"\\server\share" -> @"\\server\share\" return i; } /// /// Calls Path.GetPathRoot. If no '\\' or '/' at the end, appends "\\". /// Tested: Path.GetPathRoot returns network path like @"\\server\share". API PathSkipRoot returns with '\\'. /// internal static string GetRootBS_(string s) { s = Path.GetPathRoot(s); if (!Path.EndsInDirectorySeparator(s)) s += "\\"; return s; } /// /// Gets the length of the URL protocol name (also known as URI scheme) in string, including ':'. /// If the string does not start with a protocol name, returns 0. /// /// A URL or path or any string. Can be null. /// /// URL examples: "http:" (returns 5), "http://www.x.com" (returns 5), "file:///path" (returns 5), "shell:etc" (returns 6). /// /// The protocol can be unknown. The function just checks string format, which is an ASCII alpha character followed by one or more ASCII alpha-numeric, '.', '-', '+' characters, followed by ':' character. /// public static int getUrlProtocolLength(RStr s) { int len = s.Length; if (len > 2 && s[0].IsAsciiAlpha() && s[1] != ':') { for (int i = 1; i < len; i++) { var c = s[i]; if (c == ':') return i + 1; if (!(c.IsAsciiAlphaDigit() || c == '.' || c == '-' || c == '+')) break; } } return 0; //info: API PathIsURL lies, like most shlwapi.dll functions. } /// /// Returns true if the string starts with a URL protocol name (existing or not) and ':' character. /// Calls and returns true if it's not 0. /// /// A URL or path or any string. Can be null. /// /// URL examples: "http:", "http://www.x.com", "file:///path", "shell:etc". /// public static bool isUrl(RStr s) { return 0 != getUrlProtocolLength(s); } /// /// Combines two path parts using character '\\'. For example directory path and file name. /// /// First part. Usually a directory. /// Second part. Usually a filename or relative path. /// s2 can be full path. If it is, ignore s1 and return s2 with expanded environment variables. If false (default), simply combines s1 and s2. /// Call which may prepend @"\\?\" if the result path is very long. Default true. /// /// If s1 and s2 are null or "", returns "". Else if s1 is null or "", returns s2. Else if s2 is null or "", returns s1. /// Does not expand environment variables. For it use before, or instead. Path that starts with an environment variable here is considered not full path. /// Similar to . Main differences: has some options; supports null arguments. /// /// public static string combine(string s1, string s2, bool s2CanBeFullPath = false, bool prefixLongPath = true) { string r; if (s1.NE()) r = s2 ?? ""; else if (s2.NE()) r = s1 ?? ""; else if (s2CanBeFullPath && isFullPath(s2)) r = s2; else { int k = 0; if (IsSepChar_(s1[^1])) k |= 1; if (IsSepChar_(s2[0])) k |= 2; switch (k) { case 0: r = s1 + @"\" + s2; break; case 3: r = s1 + s2[1..]; break; default: r = s1 + s2; break; } } if (prefixLongPath) r = prefixLongPathIfNeed(r); return r; } /// /// Combines two path parts. /// Unlike , fails if some part is empty or @"\" or if s2 is @"\\". Also does not check s2 full path. /// If fails, throws exception or returns null (if noException). /// /// internal static string Combine_(string s1, string s2, bool noException = false) { if (!s1.NE() && !s2.NE()) { int k = 0; if (IsSepChar_(s1[^1])) { if (s1.Length == 1) goto ge; k |= 1; } if (IsSepChar_(s2[0])) { if (s2.Length == 1 || IsSepChar_(s2[1])) goto ge; k |= 2; } var r = k switch { 0 => s1 + @"\" + s2, 3 => s1 + s2[1..], _ => s1 + s2, }; return prefixLongPathIfNeed(r); } ge: if (noException) return null; throw new ArgumentException("Empty filename or path."); } /// /// Returns true if character c is '\\' or '/'. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool IsSepChar_(char c) { return c is '\\' or '/'; } /// /// Returns true if s starts with "\\". Supports '/'. /// /// Can be null. internal static bool StartsWithTwoSlash_(string s) => s.Lenn() >= 2 && IsSepChar_(s[0]) && IsSepChar_(s[1]); ///// ///// Returns true if ends with '\\' or '/'. ///// //internal static bool EndsWithSep_(RStr s) { return s.Length > 0 && s[^1] is '\\' or '/'; } /// /// Returns true if ends with ':' preceded by a drive letter, like "C:" or "more\C:", but not like "moreC:". /// static bool _EndsWithDriveWithoutSep(RStr s) { int i = s.Length - 1; if (i < 1 || s[i] != ':') return false; if (!s[--i].IsAsciiAlpha()) return false; if (i > 0 && !IsSepChar_(s[i - 1])) return false; return true; } /// /// If s is like "C:", returns "C:\", else returns s. /// static string _AddSepToDrive(string s) => _EndsWithDriveWithoutSep(s) ? s + "\\" : s; /// /// Makes normal full path from path that can contain special substrings etc. /// /// Any path. /// If path is not full path, combine it with defaultParentDirectory to make full path. /// /// path is not full path, and defaultParentDirectory is not used or does not make it full path. /// /// The sequence of actions: /// 1. If path starts with '%' character, expands environment variables and special folder names. See . /// 2. If path is not full path but looks like URL, and used flag CanBeUrl, returns path. /// 3. If path is not full path, and defaultParentDirectory is not null/"", combines path with expand(defaultParentDirectory). /// 4. If path is not full path, throws exception. /// 5. If path is like "C:" makes like "C:\". /// 6. Calls API GetFullPathName. It replaces '/' with '\\', replaces multiple '\\' with single (where need), processes @"\.." etc, trims spaces, etc. /// 7. If no flag DontExpandDosPath, if path looks like a short DOS path version (contains '~' etc), calls API GetLongPathName. It converts short DOS path to normal path, if possible, for example @"c:\progra~1" to @"c:\program files". It is slow. It converts path only if the file exists. /// 8. If no flag DontRemoveEndSeparator, and string ends with '\\' character, and length > 4, removes the '\\', unless then it would be a path to an existing file (not directory). /// 9. If no flag DontPrefixLongPath, calls , which adds @"\\?\" etc prefix if path is very long. /// /// Similar to . Main differences: this function expands environment variables, does not support relative paths (unless used defaultParentDirectory), trims '\\' at the end if need. /// /// public static string normalize(string path, string defaultParentDirectory = null, PNFlags flags = 0) { if (!isFullPathExpand(ref path)) { if (0 != (flags & PNFlags.CanBeUrlOrShell)) if (IsShellPathOrUrl_(path)) return path; if (defaultParentDirectory.NE()) goto ge; path = Combine_(expand(defaultParentDirectory), path); if (!isFullPath(path)) goto ge; } return Normalize_(path, flags, noExpandEV: true); ge: throw new ArgumentException($"Not full path: '{path}'."); } /// /// Same as , but skips full-path checking. /// s should be full path. If not full and not null/"", combines with current directory. /// internal static string Normalize_(string s, PNFlags flags = 0, bool noExpandEV = false) { if (!s.NE()) { if (!noExpandEV) s = expand(s); Debug_.PrintIf(IsShellPathOrUrl_(s), s); s = _AddSepToDrive(s); //API would append current directory //note: although slower, call GetFullPathName always, not just when contains @"..\" etc. // Because it does many things (see Normalize doc), not all documented. // We still ~2 times faster than Path.GetFullPath (tested before Core). Api.GetFullPathName(s, out s); if (0 == (flags & PNFlags.DontExpandDosPath) && IsPossiblyDos_(s)) s = ExpandDosPath_(s); if (0 == (flags & PNFlags.DontRemoveEndSeparator) && IsSepChar_(s[^1]) && s.Length > 4) { var s2 = s[..^1]; if (Api.GetFileAttributes(s2).Has(FileAttributes.Directory)) s = s2; //if does not exist as file } if (0 == (flags & PNFlags.DontPrefixLongPath)) s = prefixLongPathIfNeed(s); } return s; } /// /// Prepares path for passing to API and .NET functions that support "..", DOS path etc. /// Calls expand, _AddSepToDrive, . By default throws exception if !isFullPath(path). /// /// Not full path (only if throwIfNotFullPath is true). internal static string NormalizeMinimally_(string path, bool throwIfNotFullPath = true) { var s = expand(path); Debug_.PrintIf(IsShellPathOrUrl_(s), s); if (throwIfNotFullPath && !isFullPath(s)) throw new ArgumentException($"Not full path: '{path}'."); s = _AddSepToDrive(s); s = prefixLongPathIfNeed(s); return s; } /// /// Calls API GetLongPathName. /// Does not check whether s contains '~' character etc. Note: the API is slow. /// /// Can be null. internal static string ExpandDosPath_(string s) { if (!s.NE()) Api.GetLongPathName(s, out s); return s; //CONSIDER: the API fails if the file does not exist. // Workaround: if filename does not contain '~', pass only the part that contains. } /// /// Returns true if s looks like a DOS filename or path. /// Examples: "abcde~12", "abcde~12.txt", @"c:\path\abcde~12.txt", "c:\abcde~12\path". /// /// Can be null. internal static bool IsPossiblyDos_(string s) { //print.it(s); if (s != null && s.Length >= 8) { for (int i = 0; (i = s.IndexOf('~', i + 1)) > 0;) { int j = i + 1, k = 0; for (; k < 6 && j < s.Length; k++, j++) if (!s[j].IsAsciiDigit()) break; if (k == 0) continue; char c = j < s.Length ? s[j] : '\\'; if (c == '\\' || c == '/' || (c == '.' && j == s.Length - 4)) { for (j = i; j > 0; j--) { c = s[j - 1]; if (c == '\\' || c == '/') break; } if (j == i - (7 - k)) return true; } } } return false; } /// /// Returns true if starts with "::". /// /// Can be null. internal static bool IsShellPath_(RStr s) { return s.Length >= 2 && s[0] == ':' && s[1] == ':'; } /// /// Returns true if IsShellPath_(s) || isUrl(s). /// /// Can be null. internal static bool IsShellPathOrUrl_(RStr s) => IsShellPath_(s) || isUrl(s); /// /// If path is full path (see ) and does not start with @"\\?\", prepends @"\\?\". /// If path is network path (like @"\\computer\folder\..."), makes like @"\\?\UNC\computer\folder\...". /// /// /// Path. Can be null. /// Must not start with "%environmentVariable%". This function does not expand it. See . /// /// /// Windows API kernel functions support extended-length paths, ie longer than 259 characters. But the path must have this prefix. Windows API shell functions don't support it. /// public static string prefixLongPath(string path) { var s = path; if (isFullPath(s) && 0 == _GetPrefixLength(s)) { if (s.Length >= 2 && IsSepChar_(s[0]) && IsSepChar_(s[1])) s = s.ReplaceAt(0, 2, @"\\?\UNC\"); else s = @"\\?\" + s; } return s; } /// /// Calls if path is longer than (247). /// /// /// Path. Can be null. /// Must not start with "%environmentVariable%". This function does not expand it. See . /// public static string prefixLongPathIfNeed(string path) { if (path.Lenn() > maxDirectoryPathLength) path = prefixLongPath(path); return path; //info: MaxDirectoryPathLength is max length supported by API CreateDirectory. } /// /// If path starts with @"\\?\" prefix, removes it. /// If path starts with @"\\?\UNC\" prefix, removes @"?\UNC\". /// /// /// Path. Can be null. /// Must not start with "%environmentVariable%". This function does not expand it. See . /// public static string unprefixLongPath(string path) { if (!path.NE()) { switch (_GetPrefixLength(path)) { case 4: return path[4..]; case 8: return path.Remove(2, 6); } } return path; } /// /// If s starts with @"\\?\UNC\", returns 8. /// Else if starts with @"\\?\", returns 4. /// Else returns 0. /// /// Can be null. static int _GetPrefixLength(RStr s) { int len = s.Length; if (len >= 4 && s[2] == '?' && IsSepChar_(s[0]) && IsSepChar_(s[1]) && IsSepChar_(s[3])) { if (len >= 8 && IsSepChar_(s[7]) && s[4..7].Eqi("UNC")) return 8; return 4; } return 0; } /// /// Maximal file (not directory) path length supported by all functions (native, .NET and this library). /// For longer paths need @"\\?\" prefix. It is supported by most native kernel API (but not shell API) and most functions of this library and .NET. /// public const int maxFilePathLength = 259; /// /// Maximal directory path length supported by all functions (native, .NET and this library). /// For longer paths need @"\\?\" prefix. It is supported by most native kernel API (but not shell API) and most functions of this library and .NET. /// public const int maxDirectoryPathLength = 247; /// /// Replaces characters that cannot be used in file names. /// /// Initial filename. /// A string that will replace each invalid character. Default "-". /// /// Also corrects other forms of invalid or problematic filename: trims spaces and other blank characters; replaces "." at the end; prepends "@" if a reserved name like "CON" or "CON.txt"; returns "-" if name is null/empty/whitespace. /// Usually returns valid filename, however it can be too long (itself or when combined with a directory path). /// public static string correctName(string name, string invalidCharReplacement = "-") { if (name == null || (name = name.Trim()).Length == 0) return invalidCharReplacement; name = _rxInvalidFN1.Replace(name, invalidCharReplacement).Trim(); if (_rxInvalidFN2.IsMatch(name)) name = "@" + name; return name; } static regexp _rxInvalidFN1 = new(@"\.$|[\\/|<>?*:""\x00-\x1f]"); static regexp _rxInvalidFN2 = new(@"(?i)^(CON|PRN|AUX|NUL|COM\d|LPT\d)(\.|$)"); /// /// Returns true if name cannot be used for a file name, eg contains '\\' etc characters or is empty. /// More info: . /// /// Any string. Example: "name.txt". Can be null. public static bool isInvalidName(string name) { if (name == null || (name = name.Trim()).Length == 0) return true; return _rxInvalidFN1.IsMatch(name) || _rxInvalidFN2.IsMatch(name); } /// /// Returns true if character c is invalid in file names (the filename part). /// public static bool isInvalidNameChar(char c) => c is < ' ' or '"' or '<' or '>' or '|' or '*' or '?' or ':' or '\\' or '/'; /// /// Returns true if character c is invalid in file paths. /// public static bool isInvalidPathChar(char c) => c is < ' ' or '"' or '<' or '>' or '|' or '*' or '?'; /// /// Gets filename from path. Does not remove extension. /// /// Returns "" if there is no filename. Returns null if path is null. /// Path or filename. Can be null. /// /// Similar to . Some differences: if ends with '\\' or '/', gets part before it, eg "B" from @"C:\A\B\". /// /// Supports separators '\\' and '/'. /// Also supports URL and shell parsing names like @"::{CLSID-1}\0\::{CLSID-2}". /// /// Examples: /// /// | path | result /// | - /// | @"C:\A\B\file.txt" | "file.txt" /// | "file.txt" | "file.txt" /// | "file" | "file" /// | @"C:\A\B" | "B" /// | @"C:\A\B\" | "B" /// | @"C:\A\/B\/" | "B" /// | @"C:\" | "" /// | @"C:" | "" /// | @"\\network\share" | "share" /// | @"C:\aa\file.txt:alt.stream" | "file.txt:alt.stream" /// | "http://a.b.c" | "a.b.c" /// | "::{A}\::{B}" | "::{B}" /// | "" | "" /// | null | null /// public static string getName(string path) { return _GetPathPart(path, _PathPart.NameWithExt); } /// /// Gets filename without extension. /// /// Returns "" if there is no filename. Returns null if path is null. /// Path or filename (then just removes extension). Can be null. /// /// The same as , just removes extension. /// Similar to . Some differences: if ends with '\\' or '/', gets part before it, eg "B" from @"C:\A\B\". /// /// Supports separators '\\' and '/'. /// Also supports URL and shell parsing names like @"::{CLSID-1}\0\::{CLSID-2}". /// /// Examples: /// /// | path | result /// | - /// | @"C:\A\B\file.txt" | "file" /// | "file.txt" | "file" /// | "file" | "file" /// | @"C:\A\B" | "B" /// | @"C:\A\B\" | "B" /// | @"C:\A\B.B\" | "B.B" /// | @"C:\aa\file.txt:alt.stream" | "file.txt:alt" /// | "http://a.b.c" | "a.b" /// public static string getNameNoExt(string path) { return _GetPathPart(path, _PathPart.NameWithoutExt); } /// /// Gets filename extension, like ".txt". /// /// Returns "" if there is no extension. Returns null if path is null. /// Path or filename. Can be null. /// /// Supports separators '\\' and '/'. /// public static string getExtension(string path) { return _GetPathPart(path, _PathPart.Ext); } /// /// Gets filename extension and path part without the extension. /// More info: . /// /// Path or filename. Can be null. /// Receives path part without the extension. Can be the same variable as path. public static string getExtension(string path, out string pathWithoutExtension) { var ext = getExtension(path); if (ext != null && ext.Length > 0) pathWithoutExtension = path[..^ext.Length]; else pathWithoutExtension = path; return ext; } /// /// Finds filename extension, like ".txt". /// /// Index of '.' character, or -1 if there is no extension. /// Path or filename. Can be null. public static int findExtension(RStr path) { int i; for (i = path.Length; --i >= 0;) { switch (path[i]) { case '.': return i; case '\\': case '/': /*case ':':*/ return -1; } } return i; } /// /// Removes filename part from path. /// By default also removes separator ('\\' or '/') if it is not after drive name (eg "C:"). /// /// Returns "" if the string is a filename. Returns null if the string is null or a root (like @"C:\" or "C:" or @"\\server\share" or "http:"). /// Path or filename. Can be null. /// Don't remove separator character(s) ('\\' or '/'). See examples. /// /// Similar to . Some differences: skips '\\' or '/' at the end (eg from @"C:\A\B\" gets @"C:\A", not @"C:\A\B"); does not replace '/' with '\\'. /// /// Parses raw string. You may want to it at first. /// /// Supports separators '\\' and '/'. /// Also supports URL and shell parsing names like @"::{CLSID-1}\0\::{CLSID-2}". /// /// Examples: /// /// | path | result /// | - /// | @"C:\A\B\file.txt" | @"C:\A\B" /// | "file.txt" | "" /// | @"C:\A\B\" | @"C:\A" /// | @"C:\A\/B\/" | @"C:\A" /// | @"C:\" | null /// | @"\\network\share" | null /// | "http:" | null /// | @"C:\aa\file.txt:alt.stream" | "C:\aa" /// | "http://a.b.c" | "http:" /// | "::{A}\::{B}" | "::{A}" /// | "" | "" /// | null | null /// /// Examples when withSeparator true: /// /// | path | result /// | - /// | @"C:\A\B" | @"C:\A\" (not @"C:\A") /// | "http://x.y" | "http://" (not "http:") /// public static string getDirectory(string path, bool withSeparator = false) { return _GetPathPart(path, _PathPart.Dir, withSeparator); } enum _PathPart { Dir, NameWithExt, NameWithoutExt, Ext, }; static string _GetPathPart(string s, _PathPart what, bool withSeparator = false) { if (s == null) return null; int len = s.Length, i, iExt = -1; //rtrim '\\' and '/' etc for (i = len; i > 0 && IsSepChar_(s[i - 1]); i--) { if (what == _PathPart.Ext) return ""; if (what == _PathPart.NameWithoutExt) what = _PathPart.NameWithExt; } len = i; //if ends with ":" or @":\", it is either drive or URL root or invalid if (len > 0 && s[len - 1] == ':' && !IsShellPath_(s)) return (what == _PathPart.Dir) ? null : ""; //find '\\' or '/'. Also '.' if need. //Note: we don't split at ':', which could be used for alt stream or URL port or in shell parsing name as non-separator. This library does not support paths like "C:relative path". while (--i >= 0) { char c = s[i]; if (c == '.') { if (what < _PathPart.NameWithoutExt) continue; if (iExt < 0) iExt = i; if (what == _PathPart.Ext) break; } else if (c == '\\' || c == '/') { break; } } if (iExt >= 0 && iExt == len - 1) iExt = -1; //eg ends with ".." if (what == _PathPart.NameWithoutExt && iExt < 0) what = _PathPart.NameWithExt; switch (what) { case _PathPart.Ext: if (iExt >= 0) return s[iExt..]; break; case _PathPart.NameWithExt: len -= ++i; if (len == 0) return ""; return s.Substring(i, len); case _PathPart.NameWithoutExt: i++; return s[i..iExt]; case _PathPart.Dir: //skip multiple separators if (!withSeparator && i > 0) { for (; i > 0; i--) { var c = s[i - 1]; if (!(c == '\\' || c == '/')) break; } if (i == 0) return null; } if (i > 0) { //returns null if i is in root int j = getRootLength(s); if (j > 0 && IsSepChar_(s[j - 1])) j--; if (i < j) return null; if (withSeparator || _EndsWithDriveWithoutSep(s.AsSpan(0, i))) i++; return s[..i]; } break; } return ""; } /// /// Returns true if s is like ".ext" and the ext part does not contain characters .\\/: and does not start/end with whitespace. /// /// Can be null. internal static bool IsExtension_(RStr s) { if (s.Length < 2 || s[0] != '.') return false; for (int i = 1; i < s.Length; i++) { switch (s[i]) { case '.' or '\\' or '/' or ':': return false; case <= ' ' when i == 1 || i == s.Length - 1: return false; } } return true; } /// /// Returns true if s is like "protocol:" and not like "c:" or "protocol:more". /// /// Can be null. internal static bool IsProtocol_(RStr s) { return s.Length > 2 && s[^1] == ':' && getUrlProtocolLength(s) == s.Length; } /// /// Creates path with unique filename for a new file or directory. /// If the specified path is of an existing file or directory, returns path where the filename part is modified like "file 2.txt", "file 3.txt" etc. Else returns unchanged path. /// /// Suggested full path. /// The path is for a directory. The number is always appended at the very end, not before .extension. public static string makeUnique(string path, bool isDirectory) { if (!filesystem.exists(path)) return path; string ext = isDirectory ? null : getExtension(path, out path); for (int i = 2; ; i++) { var s = path + " " + i + ext; if (!filesystem.exists(s)) return s; } } } } namespace Au.Types { /// /// Flags for . /// [Flags] public enum PNFlags { /// Don't call API GetLongPathName. DontExpandDosPath = 1, /// Don't call . DontPrefixLongPath = 2, /// Don't remove \ character at the end. DontRemoveEndSeparator = 4, /// If path is not a file-system path but looks like URL (eg "http:..." or "file:...") or starts with "::", don't throw exception and don't process more (only expand environment variables). CanBeUrlOrShell = 8, } } ================================================ FILE: Au/Files, data/shortcutFile.cs ================================================ namespace Au { /// /// Creates shell shortcuts (.lnk files) and gets shortcut properties. /// public unsafe sealed class shortcutFile : IDisposable { Api.IShellLink _isl; Api.IPersistFile _ipf; string _lnkPath; bool _isOpen; bool _changedHotkey; /// /// Releases internally used COM objects (IShellLink, IPersistFile). /// public void Dispose() { if (_isl != null) { Api.ReleaseComObject(_ipf); _ipf = null; Api.ReleaseComObject(_isl); _isl = null; } } /// /// Returns the internally used IShellLink COM interface. /// internal Api.IShellLink IShellLink => _isl; //This could be public, but then need to make IShellLink public. It is defined in a non-standard way. Never mind, it is not important. shortcutFile(string lnkPath, uint mode) { _isl = new Api.ShellLink() as Api.IShellLink; _ipf = _isl as Api.IPersistFile; _lnkPath = pathname.normalize(lnkPath); if (mode != Api.STGM_WRITE && (mode == Api.STGM_READ || filesystem.exists(_lnkPath).File)) { AuException.ThrowIfHresultNot0(_ipf.Load(_lnkPath, mode), "*open"); _isOpen = true; } } /// /// Opens a shortcut file (.lnk) for getting shortcut properties. /// /// Shortcut file (.lnk) path. /// Not full path. /// Failed to open .lnk file. public static shortcutFile open(string lnkPath) { return new shortcutFile(lnkPath, Api.STGM_READ); } /// /// Creates a new instance that can be used to create or replace a shortcut file. /// /// Shortcut file (.lnk) path. /// Not full path. /// /// You can set properties and finally call . /// If the shortcut file already exists, Save replaces it. /// public static shortcutFile create(string lnkPath) { return new shortcutFile(lnkPath, Api.STGM_WRITE); } /// /// Creates a new instance that can be used to create or modify a shortcut file. /// /// Shortcut file (.lnk) path. /// Not full path. /// Failed to open existing .lnk file. /// /// Exception if file exists but cannot open it for read-write access. /// You can get and set properties and finally call . /// If the shortcut file already exists, Save updates it. /// public static shortcutFile openOrCreate(string lnkPath) { return new shortcutFile(lnkPath, Api.STGM_READWRITE); } /// /// Saves to the shortcut file (.lnk). /// /// Failed to save. /// /// Creates parent folder if need. /// public void Save() { if (_changedHotkey && !_isOpen && filesystem.exists(_lnkPath).File) _UnregisterHotkey(_lnkPath); filesystem.createDirectoryFor(_lnkPath); AuException.ThrowIfHresultNot0(_ipf.Save(_lnkPath, true), "*save"); } /// /// Gets or sets shortcut target path. /// /// null if target isn't a file system object, eg Control Panel or URL. /// The get function gets path with expanded environment variables. If possible, it corrects the target of MSI shortcuts and 64-bit Program Files shortcuts where IShellLink.GetPath lies. /// The set function failed. /// The set function allows max length 259. public string TargetPath { get => _CorrectPath(_GetString(_WhatString.Path), true); set { AuException.ThrowIfHresultNot0(_isl.SetPath(_Max259(value))); } } /// /// Gets shortcut target path and does not correct wrong MSI shortcut target. /// public string TargetPathRawMSI { get => _CorrectPath(_GetString(_WhatString.Path)); } /// /// Gets or sets a non-file-system target (eg Control Panel) through its ITEMIDLIST. /// /// /// Also can be used for any target type, but gets raw value, for example MSI shortcut target is incorrect. /// Most but not all shortcuts have this property; the get function returns null if the shortcut does not have it. /// /// The set function failed. public Pidl TargetPidl { get => (0 == _isl.GetIDList(out var pidl)) ? new Pidl(pidl) : null; set { AuException.ThrowIfHresultNot0(_isl.SetIDList(value?.UnsafePtr ?? default)); GC.KeepAlive(value); } } /// /// Gets or sets a URL target. /// Note: it is a .lnk shortcut, not a .url shortcut. /// /// The get function returns string "file:///..." if target is a file. /// The set function failed. public string TargetURL { get { if (0 != _isl.GetIDList(out var pidl)) return null; try { return Pidl.ToShellString(pidl, SIGDN.URL); } finally { Marshal.FreeCoTaskMem(pidl); } } set { TargetAnyType = value; } } /// /// Gets or sets target of any type - file/folder, URL, virtual shell object (see ). /// The string can be used with . /// /// The set function failed. public string TargetAnyType { get { var R = TargetPath; if (R != null) return R; //support MSI etc if (0 != _isl.GetIDList(out var pidl)) return null; try { return Pidl.ToString(pidl); } finally { Marshal.FreeCoTaskMem(pidl); } } set { var pidl = Pidl.FromString_(value, true); try { AuException.ThrowIfHresultNot0(_isl.SetIDList(pidl)); } finally { Marshal.FreeCoTaskMem(pidl); } } } /// /// Gets custom icon file path and icon index. /// /// null if the shortcut does not have a custom icon (then you see its target icon). /// Receives 0 or icon index or negative icon resource id. [SkipLocalsInit] public string GetIconLocation(out int iconIndex) { var b = stackalloc char[1024]; if (0 != _isl.GetIconLocation(b, 1024, out iconIndex)) return null; return _CorrectPath(new(b)); } /// /// Sets icon file path and icon index. /// /// /// 0 or icon index or negative icon resource id. /// /// Max length 259. public void SetIconLocation(string path, int iconIndex = 0) { AuException.ThrowIfHresultNot0(_isl.SetIconLocation(_Max259(path), iconIndex)); } /// /// Gets or sets the working directory path. /// /// The set function failed. /// The set function allows max length 259. public string WorkingDirectory { get => _CorrectPath(_GetString(_WhatString.WorkingDirectory)); set { AuException.ThrowIfHresultNot0(_isl.SetWorkingDirectory(_Max259(value))); } //see Description comments } /// /// Throws if s longer than 259. /// SetPath then would throw without error description. SetIconLocation would limit to 260. SetWorkingDirectory and SetDescription succeed but corrupt the .lnk file. SetArguments OK. Others not tested, rare, never mind. /// static string _Max259(string s) => s.Lenn() <= 259 ? s : throw new ArgumentException("max length 259"); /// /// Gets or sets the command-line arguments. /// /// The set function failed. public string Arguments { get => _GetString(_WhatString.Arguments); set { AuException.ThrowIfHresultNot0(_isl.SetArguments(value)); } } /// /// Gets or sets the description text (comment). /// /// The set function failed. /// The set function allows max length 259. public string Description { get => _GetString(_WhatString.Description); set { AuException.ThrowIfHresultNot0(_isl.SetDescription(_Max259(value))); } } /// /// Gets or sets hotkey. /// /// The value for the set function includes Win. /// The set function failed. public (KMod, KKey) Hotkey { get { if (0 != _isl.GetHotkey(out ushort k2)) return default; uint k = k2; return ((KMod)(k >> 8 & 7), (KKey)(k & 0xFF)); } set { var (m, k) = value; if (0 != (m & ~(KMod.Ctrl | KMod.Alt | KMod.Shift))) throw new ArgumentException("Win not supported"); AuException.ThrowIfHresultNot0(_isl.SetHotkey(Math2.MakeWord((uint)k, (uint)m))); _changedHotkey = true; } } /// /// Gets or sets the window show state. /// Most programs ignore it. /// /// 1 (normal, default), 2 (minimized) or 3 (maximized). /// The set function failed. public int ShowState { get => (0 == _isl.GetShowCmd(out var R)) ? R : Api.SW_SHOWNORMAL; set { AuException.ThrowIfHresultNot0(_isl.SetShowCmd(value)); } } //Not implemented wrappers for these IShellLink methods: //SetRelativePath, Resolve - not useful. //All are easy to call through the IShellLink property. #region public static /// /// Gets shortcut target path. It also can be a URL or a ITEMIDLIST string (see ) of a virtual shell object. /// Uses and . /// /// Shortcut file (.lnk) path. /// Failed to open. public static string getTarget(string lnkPath) { return open(lnkPath).TargetAnyType; } /// /// If shortcut file exists, unregisters its hotkey and deletes it. /// /// .lnk file path. /// Failed to unregister hotkey. /// Exceptions of . public static void delete(string lnkPath) { if (!filesystem.exists(lnkPath).File) return; _UnregisterHotkey(lnkPath); filesystem.delete(lnkPath); } #endregion #region private /// Failed to open or save. static void _UnregisterHotkey(string lnkPath) { Debug.Assert(filesystem.exists(lnkPath).File); using var x = openOrCreate(lnkPath); var k = x.Hotkey; if (k != default) { x.Hotkey = default; x.Save(); } } enum _WhatString { Path, Arguments, WorkingDirectory, Description } [SkipLocalsInit] string _GetString(_WhatString what) { using FastBuffer b = new(); for (; ; ) { int hr = 1; switch (what) { case _WhatString.Path: hr = _isl.GetPath(b.p, b.n); break; case _WhatString.Arguments: hr = _isl.GetArguments(b.p, b.n); break; case _WhatString.WorkingDirectory: hr = _isl.GetWorkingDirectory(b.p, b.n); break; case _WhatString.Description: hr = _isl.GetDescription(b.p, b.n); break; } if (hr != 0) return null; if (b.GetString(b.FindStringLength(), out var s, BSFlags.Truncates)) return s; } } string _CorrectPath(string R, bool fixMSI = false) { if (R.NE()) return null; if (!fixMSI) { R = pathname.expand(R); } else if (R.Find(@"\Installer\{") > 0) { //For MSI shortcuts GetPath gets like "C:\WINDOWS\Installer\{90110409-6000-11D3-8CFE-0150048383C9}\accicons.exe". var product = stackalloc char[40]; var component = stackalloc char[40]; if (0 != Api.MsiGetShortcutTarget(_lnkPath, product, null, component)) return null; //note: for some shortcuts MsiGetShortcutTarget gets empty component. Then MsiGetComponentPath fails. // On my PC was 1 such shortcut - Microsoft Office Excel Viewer.lnk in start menu. // Could not find a workaround. int na = 1024; var b = stackalloc char[na]; int hr = Api.MsiGetComponentPath(product, component, b, ref na); if (hr < 0) return null; //eg not installed, just advertised if (na == 0) return null; R = new(b, 0, na); //note: can be a registry key instead of file path. No such shortcuts on my PC. } //GetPath problem: replaces "c:\program files" with "c:\program files (x86)". //These don't help: SLGP_RAWPATH, GetIDList, disabled redirection. //GetWorkingDirectory and GetIconLocation get raw path, and envronment variables such as %ProgramFiles% are expanded to (x86) in 32-bit process. if (osVersion.is32BitProcessAnd64BitOS) { if (_pf == null) { string s = folders.ProgramFilesX86; _pf = s + "\\"; } if (R.Starts(_pf, true) && !filesystem.exists(R)) { var s2 = R.Remove(_pf.Length - 7, 6); if (filesystem.exists(s2)) R = s2; //info: "C:\\Program Files (x86)\\" in English, "C:\\Programme (x86)\\" in German etc. //never mind: System32 folder also has similar problem, because of redirection. //note: ShellExecuteEx also has this problem. } } return R; } static string _pf; #endregion } } ================================================ FILE: Au/Files, data/sqlite.cs ================================================ namespace Au { /// /// A SQLite database connection. /// Creates/opens/closes database file or in-memory database. Executes SQL, etc. /// /// /// This class wraps a SQLite API object sqlite3* and related sqlite3_x functions. They are documented in the SQLite website. /// /// To correctly close the database file, at first need to dispose all child objects, such as , then dispose the sqlite object. To dispose a static sqlite variable, you may want to use event. Although this class has a finalizer that disposes the object (closes database), you should always dispose explicitly. Finalizers don't run on process exit. /// /// /// /// ("guid")); /// print.it(p.GetArray("array")); /// print.it("----"); /// } /// } /// //get single value /// if(db.Get(out string s1, "SELECT name FROM test WHERE id=?", 1)) print.it(s1); else print.it("not found"); /// if(db.Get(out int i1, "PRAGMA page_size")) print.it(i1); /// ]]> /// public unsafe class sqlite : IDisposable { IntPtr _db; /// /// Opens or creates a database file. /// /// /// Database file. Can be: ///
• Full path. Supports environment variables etc, see ///
":memory:" - create a private, temporary in-memory database. ///
"" - create a private, temporary on-disk database. ///
• Starts with "file:" - see sqlite3_open_v2. /// /// sqlite3_open_v2 flags. Default: read-write, create file if does not exist (and parent directory). /// /// SQL to execute. For example, one or more ;-separated PRAGMA statements to configure the database connection. Or even "CREATE TABLE IF NOT EXISTS ...". /// This function also always executes "PRAGMA foreign_keys=ON;PRAGMA secure_delete=ON;". /// /// Not full path. /// Failed to open database or execute sql. /// /// Calls sqlite3_open_v2. /// /// Sets busy timeout 2000 ms. /// /// If a variable of this class is used by multiple threads, use lock(variable) { } where need. /// public sqlite(string file, SLFlags flags = SLFlags.ReadWriteCreate, string sql = null) { Debug.Assert(Assembly.GetCallingAssembly() != typeof(sqlite).Assembly); //don't use sqlite in Au bool isSpec = file != null && (file.Length == 0 || file == ":memory:" || file.Starts("file:")); if (!isSpec) { file = pathname.normalize(file); if (flags.Has(SLFlags.SQLITE_OPEN_CREATE) && !filesystem.exists(file, true).File) filesystem.createDirectoryFor(file); } var r = SLApi.sqlite3_open_v2(Convert2.Utf8Encode(file), ref _db, flags, null); if (r != 0) { Dispose(); throw new SLException(r, "sqlite3_open " + file); } SLApi.sqlite3_busy_timeout(_db, 2000); Execute("PRAGMA foreign_keys=ON;PRAGMA secure_delete=ON;" + sql); } /// protected virtual void Dispose(bool disposing) { if (_db != default) { var r = SLApi.sqlite3_close_v2(_db); Debug.Assert(r == 0); SLUtil_.Warn(r, "sqlite3_close"); _db = default; } } /// /// Calls sqlite3_close_v2. /// If fails, prints a warning. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// ~sqlite() => Dispose(false); /// sqlite3* public static implicit operator IntPtr(sqlite c) => c._db; /// sqlite3* public IntPtr Handle => _db; /// /// Calls sqlite3_exec to execute one or more SQL statements that don't return data. /// /// SQL statement, or several ;-separated statements. /// Failed to execute sql. public void Execute(string sql) { var b = Convert2.Utf8Encode(sql); byte* es = null; //gets better error text than sqlite3_errstr; sqlite3_errmsg gets nothing after sqlite3_exec. var r = SLApi.sqlite3_exec(_db, b, default, default, &es); if (r != 0) throw new SLException(r, "sqlite3_exec", SLUtil_.ToStringAndFree(es)); } /// /// Executes single SQL statement that does not return data. Binds values. /// /// Single SQL statement. /// /// Values that will replace ? characters in sql. /// Read about SQL parameters in SQLite website. Supported types: . Example: . /// /// Failed. /// sql contains more than single SQL statement. public void Execute(string sql, params object[] bind) { using var p = Statement(sql, bind); p.Step(); } /// /// Executes single SQL statement that does not return data. To bind values calls callback function. /// /// Single SQL statement. /// /// Callback function that should bind () values to ? characters in sql. /// Read about SQL parameters in SQLite website. /// /// Failed. /// sql contains more than single SQL statement. public void Execute(string sql, Action bind) { using var p = Statement(sql); bind(p); p.Step(); } /// /// Returns new sqliteStatement(this, sql). /// /// Single SQL statement. This function does not execute it. /// /// public sqliteStatement Statement(string sql) => new sqliteStatement(this, sql); /// /// Returns new sqliteStatement(this, sql).BindAll(bind). /// /// Single SQL statement. This function does not execute it. /// /// Values that will replace ? characters in sql. Optional. /// Read about SQL parameters in SQLite website. Supported types: . Example: . /// /// /// - sql contains more than single SQL statement. /// - A bind value is of an unsupported type. /// /// /// /// /// public sqliteStatement Statement(string sql, params object[] bind) => new sqliteStatement(this, sql).BindAll(bind); #region get /// /// Executes single SQL statement and gets single value. /// /// false if the statement returned no data. /// Receives data. /// Failed. /// sql contains more than single SQL statement. /// /// Also can be used to get uint, short, ushort, byte, sbyte, enum. Will need to cast from int. /// /// public bool Get(out int value, string sql, params object[] bind) { using var p = Statement(sql, bind); bool R = p.Step(); value = R ? p.GetInt(0) : default; return R; } /// /// Also can be used to get ulong, 64-bit enum, maybe . /// /// public bool Get(out long value, string sql, params object[] bind) { using var p = Statement(sql, bind); bool R = p.Step(); value = R ? p.GetLong(0) : default; return R; } ///// ///// //public bool Get(out DateTime value, bool convertToLocal, string sql, params object[] bind) //{ // using var p = Prepare(sql, bind); // bool R = p.Step(); // value = R ? p.GetDateTime(0, convertToLocal) : default; // return R; //} /// /// public bool Get(out bool value, string sql, params object[] bind) { using var p = Statement(sql, bind); bool R = p.Step(); value = R ? p.GetBool(0) : default; return R; } /// /// Also can be used to get float. /// /// public bool Get(out double value, string sql, params object[] bind) { using var p = Statement(sql, bind); bool R = p.Step(); value = R ? p.GetDouble(0) : default; return R; } /// /// public bool Get(out string value, string sql, params object[] bind) { using var p = Statement(sql, bind); bool R = p.Step(); value = R ? p.GetText(0) : default; return R; } /// /// public bool Get(out T[] value, string sql, params object[] bind) where T : unmanaged { using var p = Statement(sql, bind); bool R = p.Step(); value = R ? p.GetArray(0) : default; return R; } /// /// public bool Get(out List value, string sql, params object[] bind) where T : unmanaged { using var p = Statement(sql, bind); bool R = p.Step(); value = R ? p.GetList(0) : default; return R; } //note: can't add overload to get ReadOnlySpan, because it becomes invalid when the func returns. /// /// Can be used to get various value types, for example decimal, , . /// /// public bool GetStruct(out T value, string sql, params object[] bind) where T : unmanaged { using var p = Statement(sql, bind); bool R = p.Step(); value = R ? p.GetStruct(0) : default; return R; } /// /// Executes single SQL statement and returns true if it returns at least one row of data. /// /// This function is similar to the GetX functions, but it does not retrieve the data. /// public bool Any(string sql, params object[] bind) { using var p = Statement(sql, bind); return p.Step(); } #endregion /// /// Calls sqlite3_last_insert_rowid. /// public long LastInsertRowid => SLApi.sqlite3_last_insert_rowid(_db); /// /// Calls sqlite3_changes. /// public int Changes => SLApi.sqlite3_changes(_db); /// /// Calls sqlite3_get_autocommit. /// public bool IsInTransaction => 0 == SLApi.sqlite3_get_autocommit(_db); /// /// Returns new SLTransaction(this, sql, sqlOfDispose). /// See . /// /// public SLTransaction Transaction(string sql = "BEGIN", string sqlOfDispose = "ROLLBACK") => new SLTransaction(this, sql, sqlOfDispose); /// /// Returns true if the table exists. /// /// Table name. /// /// This function is slower than "CREATE TABLE IF NOT EXISTS...". /// public bool TableExists(string table) { return Any("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", table); } #region util /// /// Returns true if default database text encoding is not UTF-8. /// internal bool IsUtf16 { get { if (__isUtf16 == 0) { var t = _RawGetText("PRAGMA encoding") ?? ""; __isUtf16 = (byte)(t.Starts("UTF-16") ? 2 : 1); } return __isUtf16 == 2; } } byte __isUtf16; //0 not queried, 1 utf8 or failed, 2 utf16 string _RawGetText(string sql) { string r = null; fixed (char* p = sql) { IntPtr x = default; if (0 == SLApi.sqlite3_prepare16_v3(_db, p, -1, 0, ref x, null)) { var e = SLApi.sqlite3_step(x); Debug.Assert(e == SLError.Row || e == SLError.Done); if (e == SLError.Row) r = Convert2.Utf8Decode(SLApi.sqlite3_column_text(x, 0)); SLApi.sqlite3_finalize(x); } } return r; } #endregion } /// /// Creates and executes a SQLite prepared statement. /// /// /// This class wraps a SQLite API object sqlite3_stmt* and related sqlite3_x functions. They are documented perfectly in the SQLite website. /// More info and example: . /// A variable of this class can be used by multiple threads, but not simultaneously. Use lock(database) { } where need. /// public unsafe class sqliteStatement : IDisposable { sqlite _db; IntPtr _st; /// /// Calls sqlite3_prepare16_v3. /// /// /// Single SQL statement. /// Use flag SQLITE_PREPARE_PERSISTENT. /// Failed. /// sql contains more than single SQL statement. public sqliteStatement(sqlite db, string sql, bool persistent = false) { Not_.Null(db); _db = db; int flags = persistent ? 1 : 0; //SQLITE_PREPARE_PERSISTENT fixed (char* p = sql) { char* tail = null; _Err(SLApi.sqlite3_prepare16_v3(db, p, sql.Length * 2, flags, ref _st, &tail), "sqlite3_prepare"); if (tail != null && tail - p != sql.Length) throw new NotSupportedException("sql contains more than single SQL statement"); } } /// protected virtual void Dispose(bool disposing) { if (_st != default) { //note: don't throw, because sqlite3_finalize can return error of previous sqlite3_step etc SLApi.sqlite3_finalize(_st); _st = default; } } /// /// Calls sqlite3_finalize. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// ~sqliteStatement() => Dispose(false); /// sqlite3_stmt* public static implicit operator IntPtr(sqliteStatement s) => s._st; /// sqlite3_stmt* public IntPtr Handle => _st; /// /// The database connection. /// public sqlite DB => _db; /// /// Calls sqlite3_step, and returns true if results data available (sqlite3_step returned SQLITE_ROW). /// /// Failed. public bool Step() { var r = SLApi.sqlite3_step(_st); if (r == SLError.Row) return true; if (r != SLError.Done) _Err(r, "sqlite3_step"); return false; } /// /// Calls sqlite3_reset and/or sqlite3_clear_bindings. /// /// this. /// Call sqlite3_reset. Default true. /// Call sqlite3_clear_bindings. Default true. public sqliteStatement Reset(bool resetStatement = true, bool clearBindings = true) { //note: don't throw, because sqlite3_reset can return error of previous sqlite3_step if (resetStatement) SLApi.sqlite3_reset(_st); if (clearBindings) SLApi.sqlite3_clear_bindings(_st); return this; } #region bind int _B(SLIndexOrName p) { if (p.name == null) return p.index; int r = SLApi.sqlite3_bind_parameter_index(_st, Convert2.Utf8Encode(p.name)); if (r == 0) throw new SLException($"Parameter '{p.name}' does not exist in the SQL statement."); return r; } /// Calls sqlite3_bind_int. /// this. /// Parameter name or 1-based index. /// Failed. public sqliteStatement Bind(SLIndexOrName sqlParam, int value) => _Err(SLApi.sqlite3_bind_int(_st, _B(sqlParam), value), "sqlite3_bind_int"); /// Calls sqlite3_bind_int. /// this. /// Parameter name or 1-based index. /// Failed. public sqliteStatement Bind(SLIndexOrName sqlParam, uint value) => Bind(sqlParam, (int)value); /// Calls sqlite3_bind_int64. /// this. /// Parameter name or 1-based index. /// Failed. public sqliteStatement Bind(SLIndexOrName sqlParam, long value) => _Err(SLApi.sqlite3_bind_int64(_st, _B(sqlParam), value), "sqlite3_bind_int64"); /// Calls sqlite3_bind_int64. /// this. /// Parameter name or 1-based index. /// Failed. public sqliteStatement Bind(SLIndexOrName sqlParam, ulong value) => Bind(sqlParam, (long)value); /// Calls sqlite3_bind_int (0 or 1). /// this. /// Parameter name or 1-based index. /// Failed. public sqliteStatement Bind(SLIndexOrName sqlParam, bool value) => Bind(sqlParam, value ? 1 : 0); /// Binds an enum value as int or long. Calls sqlite3_bind_int or sqlite3_bind_int64. /// this. /// Parameter name or 1-based index. /// Failed. [MethodImpl(MethodImplOptions.NoInlining)] //ensure that value is copied to the parameter, because must not be smaller than int public sqliteStatement Bind(SLIndexOrName sqlParam, T value) where T : unmanaged, Enum => Bind(sqlParam, sizeof(T) == 8 ? *(long*)&value : *(int*)&value); /// Calls sqlite3_bind_double. /// this. /// Parameter name or 1-based index. /// Failed. public sqliteStatement Bind(SLIndexOrName sqlParam, double value) => _Err(SLApi.sqlite3_bind_double(_st, _B(sqlParam), value), "sqlite3_bind_double"); /// Calls sqlite3_bind_text16. /// this. /// Parameter name or 1-based index. /// Failed. public sqliteStatement Bind(SLIndexOrName sqlParam, string value) => _Err(SLApi.sqlite3_bind_text16(_st, _B(sqlParam), value, (value?.Length ?? 0) * 2), "sqlite3_bind_text16"); /// Calls sqlite3_bind_blob64. /// this. /// Parameter name or 1-based index. /// Failed. public sqliteStatement Bind(SLIndexOrName sqlParam, void* blob, long nBytes) => _Err(SLApi.sqlite3_bind_blob64(_st, _B(sqlParam), blob, nBytes), "sqlite3_bind_blob64"); sqliteStatement _Bind(SLIndexOrName sqlParam, RByte blob) { fixed (byte* p = blob) return Bind(sqlParam, p, blob.Length); } /// Calls sqlite3_bind_blob64. /// this. /// Parameter name or 1-based index. /// Failed. public sqliteStatement Bind(SLIndexOrName sqlParam, ReadOnlySpan blob) where T : unmanaged => _Bind(sqlParam, MemoryMarshal.AsBytes(blob)); /// Calls sqlite3_bind_blob64. /// this. /// Parameter name or 1-based index. /// Failed. public sqliteStatement Bind(SLIndexOrName sqlParam, Span blob) where T : unmanaged => _Bind(sqlParam, MemoryMarshal.AsBytes(blob)); /// Calls sqlite3_bind_blob64. /// this. /// Parameter name or 1-based index. /// Failed. public sqliteStatement Bind(SLIndexOrName sqlParam, T[] array) where T : unmanaged => Bind(sqlParam, array.AsSpan()); /// Calls sqlite3_bind_blob64. /// this. /// Parameter name or 1-based index. /// Failed. public sqliteStatement Bind(SLIndexOrName sqlParam, List list) where T : unmanaged => Bind(sqlParam, CollectionsMarshal.AsSpan(list)); /// Binds a value as blob. Calls sqlite3_bind_blob64. /// this. /// Parameter name or 1-based index. /// Failed. /// Can be any value type that does not contain fields of reference types. Examples: , , int, decimal. public sqliteStatement BindStruct(SLIndexOrName sqlParam, T value) where T : unmanaged => Bind(sqlParam, &value, sizeof(T)); /// Calls sqlite3_bind_null. /// this. /// Parameter name or 1-based index. /// Failed. /// Usually don't need to call this function. Unset parameter values are null. The Bind functions set null too if the value is null. public sqliteStatement BindNull(SLIndexOrName sqlParam) => _Err(SLApi.sqlite3_bind_null(_st, _B(sqlParam)), "sqlite3_bind_null"); //rejected. 1. Currently we don't have a Blob class. 2. Can do in SQL: zeroblob(nBytes). ///// Calls sqlite3_bind_zeroblob. ///// this. ///// Failed. //public Statement BindZeroBlob(SLIndexOrName sqlParam, int nBytes) // => _Err(SLApi.sqlite3_bind_zeroblob(_st, _B(sqlParam), nBytes), "sqlite3_bind_zeroblob"); //rejected. DateTime can be stored in many ways. Let users decide how they want to store it, and explicitly convert to long, string, etc. ///// Calls sqlite3_bind_int64(value.ToBinary()). ///// this. ///// Failed. //public Statement Bind(SLIndexOrName sqlParam, DateTime value, bool convertToUtc = false) // => Bind(sqlParam, (convertToUtc ? value.ToUniversalTime() : value).ToBinary()); /// /// Used by . /// internal void BindObject(int i, object v) { int k; switch (v) { case null: BindNull(i); break; case int x: k = x; goto gi; case uint x: k = (int)x; goto gi; case bool x: k = x ? 1 : 0; goto gi; case long x: Bind(i, x); break; case ulong x: Bind(i, x); break; case double x: Bind(i, x); break; case float x: Bind(i, x); break; case string x: Bind(i, x); break; case byte x: k = x; goto gi; case sbyte x: k = x; goto gi; case short x: k = x; goto gi; case ushort x: k = x; goto gi; case decimal x: Bind(i, &x, sizeof(decimal)); break; case Guid x: Bind(i, &x, sizeof(Guid)); break; case Enum x: switch (x.GetTypeCode()) { case TypeCode.Int64: case TypeCode.UInt64: Bind(i, Convert.ToInt64(v)); break; default: k = Convert.ToInt32(v); goto gi; } break; case Array a: //never mind: should throw if managed type. Same for List. // It seems .NET does not have a function to check it. // Slow workarounds: https://stackoverflow.com/questions/53968920/how-do-i-check-if-a-type-fits-the-unmanaged-constraint-in-c fixed (byte* p = Unsafe.As(a)) Bind(i, p, Buffer.ByteLength(a)); break; //case System.Collections.IList a: // //Can get data pointer and number of elements: // // var span = CollectionsMarshal.AsSpan(Unsafe.As>(a)). // //But how to get element type size in a safe/fast/clean way? // // This works, but unsafe etc: Marshal.SizeOf(a.GetType().GetGenericArguments()[0]) // // This does not work: MemoryMarshal.AsBytes(span). // //Or can get array through reflection, but slow and unsafe: var v=a.GetType().GetField("_items", BindingFlags.NonPublic|BindingFlags.Instance).GetValue(i) as Array; // //Or can convert to List with 'dynamic', but first time it adds 72 ms delay (hot CPU). // break; default: //never mind: this func does not support other types supported by other BindX functions. Quite difficult. var t = v.GetType(); throw new NotSupportedException(t.Name); //case DateTime x: Bind(i, x); break; } return; gi: Bind(i, k); } /// /// Binds multiple values of any supported types. /// /// this. /// /// Values that will replace ? characters in SQL. /// Read about SQL parameters in SQLite website. Example: . /// Supported types: ///
int, uint, byte, sbyte, short, ushort - calls sqlite3_bind_int. ///
bool - calls sqlite3_bind_int (0 or 1). ///
long, ulong - calls sqlite3_bind_int64. ///
double, float - calls sqlite3_bind_double. ///
string - calls sqlite3_bind_text16. ///
decimal - calls sqlite3_bind_blob64. ///
Guid - calls sqlite3_bind_blob64. ///
Array - calls sqlite3_bind_blob64. ///
• An enum type - calls sqlite3_bind_int or sqlite3_bind_int64. /// /// A value is of an unsupported type. /// Failed. /// /// For each parameter calls a sqlite3_bind_x function depending on type. Uses index 1, 2 and so on. /// This function is an alternative to calling BindX functions for each parameter. However it supports less types and adds boxing overhead. /// Does not call sqlite3_reset and sqlite3_clear_bindings. If need, call before. /// public sqliteStatement BindAll(params object[] values) { if (values != null) for (int i = 0; i < values.Length;) { object v = values[i]; BindObject(++i, v); } return this; } #endregion #region get int _C(SLIndexOrName p) { if (p.name == null) return p.index; int r = ColumnIndex(p.name); if (r < 0) throw new SLException($"Column '{p.name}' does not exist in query results."); return r; } /// /// Calls sqlite3_column_int. /// /// Column name of 0-based index in results. /// The column does not exist in query results. /// /// Use this function to get integer values of size 4, 2 or 1 bytes: int, uint, short, ushort, byte, sbyte, enum. /// public int GetInt(SLIndexOrName column) { int r = SLApi.sqlite3_column_int(_st, _C(column)); if (r == 0) _WarnGet(); return r; } /// /// Calls sqlite3_column_int64. /// /// Column name of 0-based index in results. /// The column does not exist in query results. /// /// Use this function to get integer values of size 8 bytes: long, ulong, 64-bit enum, maybe . /// public long GetLong(SLIndexOrName column) { long r = SLApi.sqlite3_column_int64(_st, _C(column)); if (r == 0) _WarnGet(); return r; } /// /// Calls sqlite3_column_int64 and returns true if the value is not 0. /// /// Column name of 0-based index in results. /// The column does not exist in query results. public bool GetBool(SLIndexOrName column) => GetLong(column) != 0; ///// ///// Calls DateTime.FromBinary(sqlite3_column_int64(column)). ///// ///// Column name of 0-based index in results. ///// If the value in database is stored as UTC, convert to local. ///// The value in database is not in the valid DateTime range. ///// //public DateTime GetDateTime(SLIndexOrName column, bool convertToLocal = false) //{ // var r = DateTime.FromBinary(GetLong(column)); //info: it's OK if 0/null // if(convertToLocal && r.Kind == DateTimeKind.Utc) r = r.ToLocalTime(); // return r; //} /// /// Calls sqlite3_column_double. /// /// Column name of 0-based index in results. /// The column does not exist in query results. public double GetDouble(SLIndexOrName column) { double r = SLApi.sqlite3_column_double(_st, _C(column)); if (r == 0) _WarnGet(); return r; } /// /// Calls sqlite3_column_text. /// /// Column name of 0-based index in results. /// The column does not exist in query results. public string GetText(SLIndexOrName column) { int icol = _C(column); if (_db.IsUtf16) { //both these codes would work, but with long strings can be significantly slower if SQLite has to convert text encoding char* t = SLApi.sqlite3_column_text16(_st, icol); if (t != null) return new string(t, 0, SLApi.sqlite3_column_bytes16(_st, icol)); } else { byte* t = SLApi.sqlite3_column_text(_st, icol); if (t != null) return Encoding.UTF8.GetString(t, SLApi.sqlite3_column_bytes(_st, icol)); } _WarnGet(); return null; } /// /// Calls sqlite3_column_blob. /// /// The returned memory is managed by SQLite and will become invalid when calling other SQLite functions afterwards. For zero-length BLOB returns null. /// Column name of 0-based index in results. /// Blob size. /// The column does not exist in query results. public byte* GetBlob(SLIndexOrName column, out int nBytes) { int icol = _C(column); var r = (byte*)SLApi.sqlite3_column_blob(_st, icol); if (r == null) { nBytes = 0; _WarnGet(); } else nBytes = SLApi.sqlite3_column_bytes(_st, icol); return r; } /// /// Calls sqlite3_column_blob. /// /// The returned memory is managed by SQLite and will become invalid when calling other SQLite functions afterwards. /// Column name of 0-based index in results. /// The column does not exist in query results. public RByte GetBlob(SLIndexOrName column) { var p = GetBlob(column, out int n); return new(p, n); } /// /// Calls sqlite3_column_blob. /// /// The returned memory is managed by SQLite and will become invalid when calling other SQLite functions afterwards. /// Column name of 0-based index in results. /// The column does not exist in query results. public ReadOnlySpan GetBlob(SLIndexOrName column) where T : unmanaged { var p = GetBlob(column, out int n); return new(p, n / sizeof(T)); } /// /// Calls sqlite3_column_blob and creates array. /// /// Column name of 0-based index in results. /// The column does not exist in query results. public T[] GetArray(SLIndexOrName column) where T : unmanaged { var t = GetBlob(column, out int nb); if (t == null) return null; if (nb == 0) return []; int size = sizeof(T); int ne = nb / size; nb = ne * size; var r = new T[ne]; fixed (T* p = r) MemoryUtil.Copy(t, p, nb); return r; } /// /// Calls sqlite3_column_blob and creates List<T>. /// /// Column name of 0-based index in results. /// The column does not exist in query results. public List GetList(SLIndexOrName column) where T : unmanaged { var t = (T*)GetBlob(column, out int nb); if (t == null) return null; int ne = nb / sizeof(T); var r = new List(ne); for (int i = 0; i < ne; i++) r.Add(t[i]); return r; } /// /// Calls sqlite3_column_blob and creates a variable of any value type that does not have fields of reference types. /// /// Column name of 0-based index in results. /// The column does not exist in query results. public T GetStruct(SLIndexOrName column) where T : unmanaged { var t = (T*)GetBlob(column, out int nb); T r = default; if (t != null && nb == sizeof(T)) MemoryUtil.Copy(t, &r, nb); return r; } /// /// Calls sqlite3_column_count. /// public int ColumnCount => SLApi.sqlite3_column_count(_st); /// /// Calls sqlite3_column_name. /// public string ColumnName(int index) => Convert2.Utf8Decode(SLApi.sqlite3_column_name(_st, index)); /// /// Finds column by name in results. /// /// 0-based index, or -1 if not found. /// Column name in results, as returned by sqlite3_column_name. Case-sensitive. [SkipLocalsInit] public int ColumnIndex(string name) { int n = ColumnCount; if (n > 0 && !name.NE() && !name.Contains('\0')) { Span sa = stackalloc byte[name.Length * 3]; var bname = sa[..Encoding.UTF8.GetBytes(name, sa)]; for (int i = 0; i < n; i++) { byte* b = SLApi.sqlite3_column_name(_st, i); if (*b == bname[0] && MemoryMarshal.CreateReadOnlySpanFromNullTerminated(b).SequenceEqual(bname)) return i; } } return -1; } #endregion #region util [DebuggerStepThrough] sqliteStatement _Err(SLError r, string func) { if (r != 0 && r != SLError.Row) throw new SLException(r, _db, func); return this; } //rejected: not thread-safe. Other .NET wrappers ignore it too. //void _Err(string func) //{ // var r = SLApi.sqlite3_errcode(_db); // if(r != 0 && r != SLError.Row) throw new SLException(r, _db, func); //} /// /// Called by GetX functions when sqlite3_column_x returns null/0. /// Shows warning if sqlite3_errcode is not 0 or Row. /// Does not throw exception because it is not thread-safe. /// void _WarnGet([CallerMemberName] string m_ = null) { var r = SLApi.sqlite3_errcode(_db); if (r != 0 && r != SLError.Row) SLUtil_.Warn(r, m_, "Note: it may be a false positive if this database connection is used by multiple threads simultaneously without locking."); } #endregion } } namespace Au.Types { /// /// A SQLite transaction or savepoint. The main purpose is to automatically rollback if not explicitly committed. /// Usage: using(var trans = new SLTransaction(db)) { ... trans.Commit(); } /// public sealed class SLTransaction : IDisposable { sqlite _db; /// /// Begins a SQLite transaction and prepares for automatic rollback if not explicitly committed. /// Usage: using(var trans = new SLTransaction(db)) { ... trans.Commit(); } /// /// /// SQL to execute now. Default "BEGIN". For nested transaction use "SAVEPOINT name". /// /// SQL to execute when disposing the variable if not called or . /// Default "ROLLBACK". For nested transaction use "ROLLBACK TO name". /// See also: . /// /// Failed to execute sql. public SLTransaction(sqlite db, string sql = "BEGIN", string sqlOfDispose = "ROLLBACK") { Not_.Null(db); db.Execute(sql); _db = db; SqlOfDispose = sqlOfDispose; } /// /// Calls if not called or . /// /// Failed to execute . public void Dispose() { if (_db != null) Rollback(SqlOfDispose); } /// ~SLTransaction() { if (_db != null) print.warning($"Non-disposed SLTransaction variable."); } /// /// Executes a rollback SQL (if in transaction) and disables . /// Usually don't need to call this function explicitly. It is implicitly called when disposing this variable if the transaction was not committed. /// /// SQL to execute. Default: . /// Failed to execute sql. public void Rollback(string sql = null) { if (_db == null) throw new InvalidOperationException(); if (sql == null) sql = SqlOfDispose; //if(ErrorMessage!=null) print.it(ErrorMessage); if (_db.IsInTransaction) //in some cases sqlite rolls back on error _db.Execute(sql); _db = null; } /// /// Executes a commit SQL and disables . /// /// SQL to execute. Default "COMMIT". For nested transaction use "RELEASE name". /// Failed to execute sql. public void Commit(string sql = "COMMIT") { if (_db == null) throw new InvalidOperationException(); _db.Execute(sql); _db = null; } /// /// Gets or sets SQL to execute when disposing this variable if not called or . /// Initially = parameter sqlOfDispose of constructor. /// public string SqlOfDispose { get; set; } //public string ErrorMessage { get; set; } } /// /// Used for parameter types of some functions. /// Has implicit conversions from int and string. If int, the value is interpreted as index. If string - as name. /// public struct SLIndexOrName { #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member public int index; public string name; SLIndexOrName(int index) { this.index = index; this.name = null; } SLIndexOrName(string name) { this.index = 0; this.name = name; } public static implicit operator SLIndexOrName(int index) => new(index); public static implicit operator SLIndexOrName(string name) => new(name); #pragma warning restore CS1591 } /// /// Exception thrown by , and related types. /// public class SLException : Exception { #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member public SLException(string message) : base(message) { } internal SLException(SLError r, string prefix = null, string suffix = null) : base(SLUtil_.Concat(prefix, SLApi.Errstr(r), suffix)) { Debug.Assert(r != 0 && r != SLError.Done && r != SLError.Row); ErrorCode = r; } internal SLException(SLError r, IntPtr db, string prefix = null) : base(SLUtil_.Concat(prefix, r, db)) { Debug.Assert(r != 0 && r != SLError.Done && r != SLError.Row); ErrorCode = r; } #pragma warning restore CS1591 /// /// The called SQLite API function returned this error code. /// public SLError ErrorCode { get; private set; } } } namespace Au.More { internal static unsafe class SLUtil_ { internal static string ToStringAndFree(byte* utf8) { var r = Convert2.Utf8Decode(utf8); SLApi.sqlite3_free(utf8); return r; } //currently not used //internal static void Err(SLError r, sqlite db, string func) //{ // if(r != 0) throw new SLException(r, db, func); //} internal static void Warn(SLError r, string func, string suffix = null) { if (r != 0) print.warning(SLUtil_.Concat(func, SLApi.Errstr(r), suffix)); } internal static string Concat(string s1, string s2, string s3) { using (new StringBuilder_(out var b)) { _Append(s1); _Append(s2); if (s3 != s2) _Append(s3); return b.ToString(); void _Append(string s) => b.AppendSentence(s, s?.Starts("sqlite3_") ?? false); //avoid uppercase for function names } } internal static string Concat(string s1, SLError r, IntPtr db) => Concat(s1, SLApi.Errstr(r), SLApi.sqlite3_errcode(db) == 0 ? null : SLApi.Errmsg(db)); } } ================================================ FILE: Au/Files, data/sqlite_api.cs ================================================ using System.IO.Compression; namespace Au.Types; #region enum //Most of these declarations are from System.Data.SQLite library. Modified. /// /// Flags for constructor. /// [Flags] public enum SLFlags { /// Default flags. SQLITE_OPEN_READWRITE and SQLITE_OPEN_CREATE. ReadWriteCreate = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member SQLITE_OPEN_READONLY = 0x1, SQLITE_OPEN_READWRITE = 0x2, SQLITE_OPEN_CREATE = 0x4, SQLITE_OPEN_URI = 0x40, SQLITE_OPEN_NOMUTEX = 0x8000, SQLITE_OPEN_FULLMUTEX = 0x10000, SQLITE_OPEN_SHAREDCACHE = 0x20000, SQLITE_OPEN_PRIVATECACHE = 0x40000, #pragma warning restore CS1591 } /// /// SQLite API error codes. Also two success codes - Row and Done. /// public enum SLError { /// /// Successful result /// Ok /* 0 */, /// /// SQL error or missing database /// Error /* 1 */, /// /// Internal logic error in SQLite /// Internal /* 2 */, /// /// Access permission denied /// Perm /* 3 */, /// /// Callback routine requested an abort /// Abort /* 4 */, /// /// The database file is locked /// Busy /* 5 */, /// /// A table in the database is locked /// Locked /* 6 */, /// /// A malloc() failed /// NoMem /* 7 */, /// /// Attempt to write a readonly database /// ReadOnly /* 8 */, /// /// Operation terminated by sqlite3_interrupt /// Interrupt /* 9 */, /// /// Some kind of disk I/O error occurred /// IoErr /* 10 */, /// /// The database disk image is malformed /// Corrupt /* 11 */, /// /// Unknown opcode in sqlite3_file_control /// NotFound /* 12 */, /// /// Insertion failed because database is full /// Full /* 13 */, /// /// Unable to open the database file /// CantOpen /* 14 */, /// /// Database lock protocol error /// Protocol /* 15 */, /// /// Database is empty /// Empty /* 16 */, /// /// The database schema changed /// Schema /* 17 */, /// /// String or BLOB exceeds size limit /// TooBig /* 18 */, /// /// Abort due to constraint violation /// Constraint /* 19 */, /// /// Data type mismatch /// Mismatch /* 20 */, /// /// Library used incorrectly /// Misuse /* 21 */, /// /// Uses OS features not supported on host /// NoLfs /* 22 */, /// /// Authorization denied /// Auth /* 23 */, /// /// Auxiliary database format error /// Format /* 24 */, /// /// 2nd parameter to sqlite3_bind out of range /// Range /* 25 */, /// /// File opened that is not a database file /// NotADb /* 26 */, /// /// Notifications from sqlite3_log /// Notice /* 27 */, /// /// Warnings from sqlite3_log /// Warning /* 28 */, /// /// sqlite3_step has another row ready /// Row = 100, /// /// sqlite3_step has finished executing /// Done /* 101 */, ///// ///// Used to mask off extended result codes ///// //NonExtendedMask = 0xFF, #if false ///////////////////////////////////////////////////////////////////////// // BEGIN EXTENDED RESULT CODES ///////////////////////////////////////////////////////////////////////// /// /// A collation sequence was referenced by a schema and it cannot be /// found. /// Error_Missing_CollSeq = (Error | (1 << 8)), /// /// An internal operation failed and it may succeed if retried. /// Error_Retry = (Error | (2 << 8)), /// /// A file read operation failed. /// IoErr_Read = (IoErr | (1 << 8)), /// /// A file read operation returned less data than requested. /// IoErr_Short_Read = (IoErr | (2 << 8)), /// /// A file write operation failed. /// IoErr_Write = (IoErr | (3 << 8)), /// /// A file synchronization operation failed. /// IoErr_Fsync = (IoErr | (4 << 8)), /// /// A directory synchronization operation failed. /// IoErr_Dir_Fsync = (IoErr | (5 << 8)), /// /// A file truncate operation failed. /// IoErr_Truncate = (IoErr | (6 << 8)), /// /// A file metadata operation failed. /// IoErr_Fstat = (IoErr | (7 << 8)), /// /// A file unlock operation failed. /// IoErr_Unlock = (IoErr | (8 << 8)), /// /// A file lock operation failed. /// IoErr_RdLock = (IoErr | (9 << 8)), /// /// A file delete operation failed. /// IoErr_Delete = (IoErr | (10 << 8)), /// /// Not currently used. /// IoErr_Blocked = (IoErr | (11 << 8)), /// /// Out-of-memory during a file operation. /// IoErr_NoMem = (IoErr | (12 << 8)), /// /// A file existence/status operation failed. /// IoErr_Access = (IoErr | (13 << 8)), /// /// A check for a reserved lock failed. /// IoErr_CheckReservedLock = (IoErr | (14 << 8)), /// /// A file lock operation failed. /// IoErr_Lock = (IoErr | (15 << 8)), /// /// A file close operation failed. /// IoErr_Close = (IoErr | (16 << 8)), /// /// A directory close operation failed. /// IoErr_Dir_Close = (IoErr | (17 << 8)), /// /// A shared memory open operation failed. /// IoErr_ShmOpen = (IoErr | (18 << 8)), /// /// A shared memory size operation failed. /// IoErr_ShmSize = (IoErr | (19 << 8)), /// /// A shared memory lock operation failed. /// IoErr_ShmLock = (IoErr | (20 << 8)), /// /// A shared memory map operation failed. /// IoErr_ShmMap = (IoErr | (21 << 8)), /// /// A file seek operation failed. /// IoErr_Seek = (IoErr | (22 << 8)), /// /// A file delete operation failed because it does not exist. /// IoErr_Delete_NoEnt = (IoErr | (23 << 8)), /// /// A file memory mapping operation failed. /// IoErr_Mmap = (IoErr | (24 << 8)), /// /// The temporary directory path could not be obtained. /// IoErr_GetTempPath = (IoErr | (25 << 8)), /// /// A path string conversion operation failed. /// IoErr_ConvPath = (IoErr | (26 << 8)), /// /// Reserved. /// IoErr_VNode = (IoErr | (27 << 8)), /// /// An attempt to authenticate failed. /// IoErr_Auth = (IoErr | (28 << 8)), /// /// An attempt to begin a file system transaction failed. /// IoErr_Begin_Atomic = (IoErr | (29 << 8)), /// /// An attempt to commit a file system transaction failed. /// IoErr_Commit_Atomic = (IoErr | (30 << 8)), /// /// An attempt to rollback a file system transaction failed. /// IoErr_Rollback_Atomic = (IoErr | (31 << 8)), /// /// A database table is locked in shared-cache mode. /// Locked_SharedCache = (Locked | (1 << 8)), /// /// A virtual table in the database is locked. /// Locked_Vtab = (Locked | (2 << 8)), /// /// A database file is locked due to a recovery operation. /// Busy_Recovery = (Busy | (1 << 8)), /// /// A database file is locked due to snapshot semantics. /// Busy_Snapshot = (Busy | (2 << 8)), /// /// A database file cannot be opened because no temporary directory is available. /// CantOpen_NoTempDir = (CantOpen | (1 << 8)), /// /// A database file cannot be opened because its path represents a directory. /// CantOpen_IsDir = (CantOpen | (2 << 8)), /// /// A database file cannot be opened because its full path could not be obtained. /// CantOpen_FullPath = (CantOpen | (3 << 8)), /// /// A database file cannot be opened because a path string conversion operation failed. /// CantOpen_ConvPath = (CantOpen | (4 << 8)), /// /// A virtual table is malformed. /// Corrupt_Vtab = (Corrupt | (1 << 8)), /// /// A required sequence table is missing or corrupt. /// Corrupt_Sequence = (Corrupt | (2 << 8)), /// /// A database file is read-only due to a recovery operation. /// ReadOnly_Recovery = (ReadOnly | (1 << 8)), /// /// A database file is read-only because a lock could not be obtained. /// ReadOnly_CantLock = (ReadOnly | (2 << 8)), /// /// A database file is read-only because it needs rollback processing. /// ReadOnly_Rollback = (ReadOnly | (3 << 8)), /// /// A database file is read-only because it was moved while open. /// ReadOnly_DbMoved = (ReadOnly | (4 << 8)), /// /// The shared-memory file is read-only and it should be read-write. /// ReadOnly_CantInit = (ReadOnly | (5 << 8)), /// /// Unable to create journal file because the directory is read-only. /// ReadOnly_Directory = (ReadOnly | (6 << 8)), /// /// An operation is being aborted due to rollback processing. /// Abort_Rollback = (Abort | (2 << 8)), /// /// A CHECK constraint failed. /// Constraint_Check = (Constraint | (1 << 8)), /// /// A commit hook produced a unsuccessful return code. /// Constraint_CommitHook = (Constraint | (2 << 8)), /// /// A FOREIGN KEY constraint failed. /// Constraint_ForeignKey = (Constraint | (3 << 8)), /// /// Not currently used. /// Constraint_Function = (Constraint | (4 << 8)), /// /// A NOT NULL constraint failed. /// Constraint_NotNull = (Constraint | (5 << 8)), /// /// A PRIMARY KEY constraint failed. /// Constraint_PrimaryKey = (Constraint | (6 << 8)), /// /// The RAISE function was used by a trigger-program. /// Constraint_Trigger = (Constraint | (7 << 8)), /// /// A UNIQUE constraint failed. /// Constraint_Unique = (Constraint | (8 << 8)), /// /// Not currently used. /// Constraint_Vtab = (Constraint | (9 << 8)), /// /// A ROWID constraint failed. /// Constraint_RowId = (Constraint | (10 << 8)), /// /// Frames were recovered from the WAL log file. /// Notice_Recover_Wal = (Notice | (1 << 8)), /// /// Pages were recovered from the journal file. /// Notice_Recover_Rollback = (Notice | (2 << 8)), /// /// An automatic index was created to process a query. /// Warning_AutoIndex = (Warning | (1 << 8)), /// /// User authentication failed. /// Auth_User = (Auth | (1 << 8)), /// /// Success. Prevents the extension from unloading until the process /// terminates. /// Ok_Load_Permanently = (Ok | (1 << 8)) #endif } #if false // These are the options to the internal sqlite3_config call. internal enum Config { SQLITE_CONFIG_NONE = 0, // nil SQLITE_CONFIG_SINGLETHREAD = 1, // nil SQLITE_CONFIG_MULTITHREAD = 2, // nil SQLITE_CONFIG_SERIALIZED = 3, // nil SQLITE_CONFIG_MALLOC = 4, // sqlite3_mem_methods* SQLITE_CONFIG_GETMALLOC = 5, // sqlite3_mem_methods* SQLITE_CONFIG_SCRATCH = 6, // void*, int sz, int N SQLITE_CONFIG_PAGECACHE = 7, // void*, int sz, int N SQLITE_CONFIG_HEAP = 8, // void*, int nByte, int min SQLITE_CONFIG_MEMSTATUS = 9, // boolean SQLITE_CONFIG_MUTEX = 10, // sqlite3_mutex_methods* SQLITE_CONFIG_GETMUTEX = 11, // sqlite3_mutex_methods* // previously SQLITE_CONFIG_CHUNKALLOC 12 which is now unused SQLITE_CONFIG_LOOKASIDE = 13, // int int SQLITE_CONFIG_PCACHE = 14, // sqlite3_pcache_methods* SQLITE_CONFIG_GETPCACHE = 15, // sqlite3_pcache_methods* SQLITE_CONFIG_LOG = 16, // xFunc, void* SQLITE_CONFIG_URI = 17, // int SQLITE_CONFIG_PCACHE2 = 18, // sqlite3_pcache_methods2* SQLITE_CONFIG_GETPCACHE2 = 19, // sqlite3_pcache_methods2* SQLITE_CONFIG_COVERING_INDEX_SCAN = 20, // int SQLITE_CONFIG_SQLLOG = 21, // xSqllog, void* SQLITE_CONFIG_MMAP_SIZE = 22, // sqlite3_int64, sqlite3_int64 SQLITE_CONFIG_WIN32_HEAPSIZE = 23, // int nByte SQLITE_CONFIG_PCACHE_HDRSZ = 24, // int *psz SQLITE_CONFIG_PMASZ = 25 // unsigned int szPma } internal enum ConfigDb { /// /// This value represents an unknown (or invalid) option, do not use it. /// SQLITE_DBCONFIG_NONE = 0, // nil /// /// This option is used to change the name of the "main" database /// schema. The sole argument is a pointer to a constant UTF-8 string /// which will become the new schema name in place of "main". /// SQLITE_DBCONFIG_MAINDBNAME = 1000, // char* /// /// This option is used to configure the lookaside memory allocator. /// The value must be an array with three elements. The second element /// must be an containing the size of each buffer /// slot. The third element must be an containing /// the number of slots. The first element must be an /// that points to a native memory buffer of bytes equal to or greater /// than the product of the second and third element values. /// SQLITE_DBCONFIG_LOOKASIDE = 1001, // void* int int /// /// This option is used to enable or disable the enforcement of /// foreign key constraints. /// SQLITE_DBCONFIG_ENABLE_FKEY = 1002, // int int* /// /// This option is used to enable or disable triggers. /// SQLITE_DBCONFIG_ENABLE_TRIGGER = 1003, // int int* /// /// This option is used to enable or disable the two-argument version /// of the fts3_tokenizer function which is part of the FTS3 full-text /// search engine extension. /// SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER = 1004, // int int* /// /// This option is used to enable or disable the loading of extensions. /// SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION = 1005, // int int* /// /// This option is used to enable or disable the automatic checkpointing /// when a WAL database is closed. /// SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE = 1006, // int int* /// /// This option is used to enable or disable the query planner stability /// guarantee (QPSG). /// SQLITE_DBCONFIG_ENABLE_QPSG = 1007, // int int* /// /// This option is used to enable or disable the extra EXPLAIN QUERY PLAN /// output for trigger programs. /// SQLITE_DBCONFIG_TRIGGER_EQP = 1008, // int int* /// /// This option is used as part of the process to reset a database back /// to an empty state. Because resetting a database is destructive and /// irreversible, the process requires the use of this obscure flag and /// multiple steps to help ensure that it does not happen by accident. /// SQLITE_DBCONFIG_RESET_DATABASE = 1009 // int int* } internal enum TypeAffinity { /// /// All integers in SQLite default to Int64 /// Int64 = 1, /// /// All floating point numbers in SQLite default to double /// Double = 2, /// /// The default data type of SQLite is text /// Text = 3, /// /// Typically blob types are only seen when returned from a function /// Blob = 4, /// /// Null types can be returned from functions /// Null = 5, } #endif #endregion #region functions internal static unsafe class SLApi { const string SQLITE_DLL = "winsqlite3.dll"; static SLApi() { //Windows 10 and 11 have winsqlite3.dll. On other OS download from www.sqlite.org. if (!NativeLibrary.TryLoad(SQLITE_DLL, out _)) _LoadDownloaded(); static void _LoadDownloaded() { string path = folders.ThisAppDataLocal; if (path.Ends(@"\_script", true)) path = path[..^8]; path += $@"\download\{SQLITE_DLL}"; //print.it(path, filesystem.exists(path)); if (!filesystem.exists(path)) { gRetry: try { _Download(path); } catch (Exception ex) { int r = dialog.showError("Failed to download missing files", $"Your OS does not have this file: {SQLITE_DLL}.\r\nThis program tried to download it from www.sqlite.org, but failed.", "1 Retry|0 Cancel", expandedText: ex.ToString()); if (r == 1) goto gRetry; Environment.Exit(0); } } NativeLibrary.Load(path); static void _Download(string path) { //Get the download URL of the last sqlite version for this OS. This way is documented in https://www.sqlite.org/download.html. var html = internet.http_.Get("https://www.sqlite.org/download.html").Text(); //print.it(html); if (!html.RxMatch(@"(?s)", 1, out string csv)) throw new AuException(); //print.it(csv); var x = csvTable.parse(csv); string s = $"sqlite-dll-win-{RuntimeInformation.ProcessArchitecture.ToString().Lower()}"; string url = x.Rows.First(o => o[2].Contains(s, StringComparison.OrdinalIgnoreCase))[2]; url = "https://www.sqlite.org/" + url; //print.it(url); //Download and extract. var bytes = internet.http_.Get(url).Bytes(); using var za = new ZipArchive(new MemoryStream(bytes), ZipArchiveMode.Read); var e = za.GetEntry("sqlite3.dll"); filesystem.createDirectoryFor(path); e.ExtractToFile(path, true); } } } [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern SLError sqlite3_close_v2(IntPtr db); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_create_function(IntPtr db, byte[] strName, int nArgs, int nType, IntPtr pvUser, SQLiteCallback func, SQLiteCallback fstep, SQLiteFinalCallback ffinal); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern SLError sqlite3_finalize(IntPtr stmt); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_backup_finish(IntPtr backup); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern SLError sqlite3_reset(IntPtr stmt); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern byte* sqlite3_bind_parameter_name(IntPtr stmt, int index); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern byte* sqlite3_column_database_name(IntPtr stmt, int index); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern byte* sqlite3_column_decltype(IntPtr stmt, int index); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern byte* sqlite3_column_name(IntPtr stmt, int index); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern byte* sqlite3_column_origin_name(IntPtr stmt, int index); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern byte* sqlite3_column_table_name(IntPtr stmt, int index); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern byte* sqlite3_column_text(IntPtr stmt, int index); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern char* sqlite3_column_text16(IntPtr stmt, int index); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern byte* sqlite3_errmsg(IntPtr db); internal static string Errmsg(IntPtr db) => Convert2.Utf8Decode(sqlite3_errmsg(db)); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern SLError sqlite3_prepare16_v3(IntPtr db, char* sql, int nBytes, int prepFlags, ref IntPtr stmt, char** ptrRemain); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_table_column_metadata(IntPtr db, byte[] dbName, byte[] tblName, byte[] colName, byte** ptrDataType, byte** ptrCollSeq, ref int notNull, ref int primaryKey, ref int autoInc); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern byte* sqlite3_value_text(IntPtr p); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern byte* sqlite3_libversion(); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern int sqlite3_libversion_number(); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern byte* sqlite3_sourceid(); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern int sqlite3_compileoption_used(byte[] zOptName); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern byte* sqlite3_compileoption_get(int N); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_enable_shared_cache(int enable); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_enable_load_extension(IntPtr db, int enable); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_load_extension(IntPtr db, byte[] fileName, byte[] procName, byte** pError); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_overload_function(IntPtr db, byte[] zName, int nArgs); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_win32_set_directory16(uint type, string value); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_win32_reset_heap(); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_win32_compact_heap(ref uint largest); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern void* sqlite3_malloc(int n); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern void* sqlite3_malloc64(ulong n); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern void* sqlite3_realloc(void* p, int n); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern void* sqlite3_realloc64(void* p, ulong n); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern ulong sqlite3_msize(void* p); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern void sqlite3_free(void* p); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern SLError sqlite3_open_v2(byte[] filename, ref IntPtr db, SLFlags flags, byte[] vfsName); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern void sqlite3_interrupt(IntPtr db); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern long sqlite3_last_insert_rowid(IntPtr db); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern int sqlite3_changes(IntPtr db); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern long sqlite3_memory_used(); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern long sqlite3_memory_highwater(int resetFlag); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_shutdown(); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern SLError sqlite3_busy_timeout(IntPtr db, int ms); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern SLError sqlite3_clear_bindings(IntPtr stmt); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] static extern SLError sqlite3_bind_blob64(IntPtr stmt, int index, void* value, long nSize, nint nTransient); internal static SLError sqlite3_bind_blob64(IntPtr stmt, int index, void* value, long nSize) => sqlite3_bind_blob64(stmt, index, value, nSize, -1); //SQLITE_TRANSIENT //FUTURE: somehow avoid copying if large data. Eg if array or List, can use callback and GCHandle. [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern SLError sqlite3_bind_double(IntPtr stmt, int index, double value); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern SLError sqlite3_bind_int(IntPtr stmt, int index, int value); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern SLError sqlite3_bind_int64(IntPtr stmt, int index, long value); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern SLError sqlite3_bind_null(IntPtr stmt, int index); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] static extern SLError sqlite3_bind_text16(IntPtr stmt, int index, string value, int nlen, nint nTransient); internal static SLError sqlite3_bind_text16(IntPtr stmt, int index, string value, int nlen) => sqlite3_bind_text16(stmt, index, value, nlen, -1); //SQLITE_TRANSIENT //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_bind_zeroblob(IntPtr stmt, int index, int n); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern int sqlite3_bind_parameter_count(IntPtr stmt); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern int sqlite3_bind_parameter_index(IntPtr stmt, byte[] strName); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern int sqlite3_column_count(IntPtr stmt); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern SLError sqlite3_step(IntPtr stmt); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern int sqlite3_stmt_readonly(IntPtr stmt); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern double sqlite3_column_double(IntPtr stmt, int index); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern int sqlite3_column_int(IntPtr stmt, int index); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern long sqlite3_column_int64(IntPtr stmt, int index); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern void* sqlite3_column_blob(IntPtr stmt, int index); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern int sqlite3_column_bytes(IntPtr stmt, int index); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern int sqlite3_column_bytes16(IntPtr stmt, int index); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern TypeAffinity sqlite3_column_type(IntPtr stmt, int index); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_create_collation(IntPtr db, byte[] strName, int nType, IntPtr pvUser, SQLiteCollation func); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern void* sqlite3_value_blob(IntPtr p); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern int sqlite3_value_bytes(IntPtr p); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern int sqlite3_value_bytes16(IntPtr p); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern double sqlite3_value_double(IntPtr p); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern int sqlite3_value_int(IntPtr p); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern long sqlite3_value_int64(IntPtr p); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern TypeAffinity sqlite3_value_type(IntPtr p); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern void sqlite3_result_blob(IntPtr context, byte[] value, int nSize, IntPtr pvReserved); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern void sqlite3_result_double(IntPtr context, double value); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern void sqlite3_result_error(IntPtr context, byte[] strErr, int nLen); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern void sqlite3_result_error_code(IntPtr context, SLError value); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern void sqlite3_result_error_toobig(IntPtr context); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern void sqlite3_result_error_nomem(IntPtr context); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern void sqlite3_result_value(IntPtr context, IntPtr value); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern void sqlite3_result_zeroblob(IntPtr context, int nLen); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern void sqlite3_result_int(IntPtr context, int value); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern void sqlite3_result_int64(IntPtr context, long value); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern void sqlite3_result_null(IntPtr context); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern IntPtr sqlite3_aggregate_context(IntPtr context, int nBytes); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern void sqlite3_result_error16(IntPtr context, string strName, int nLen); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern void sqlite3_result_text16(IntPtr context, string strName, int nLen, IntPtr pvReserved); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern void sqlite3_progress_handler(IntPtr db, int ops, SQLiteProgressCallback func, IntPtr pvUser); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern IntPtr sqlite3_set_authorizer(IntPtr db, SQLiteAuthorizerCallback func, IntPtr pvUser); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern IntPtr sqlite3_update_hook(IntPtr db, SQLiteUpdateCallback func, IntPtr pvUser); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern IntPtr sqlite3_commit_hook(IntPtr db, SQLiteCommitCallback func, IntPtr pvUser); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern IntPtr sqlite3_trace(IntPtr db, SQLiteTraceCallback func, IntPtr pvUser); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern IntPtr sqlite3_trace_v2(IntPtr db, SQLiteTraceFlags mask, SQLiteTraceCallback2 func, IntPtr pvUser); // Since sqlite3_config() takes a variable argument list, we have to overload declarations // for all possible calls that we want to use. //[DllImport(SQLITE_DLL, EntryPoint = "sqlite3_config", CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_config_none(Config op); //[DllImport(SQLITE_DLL, EntryPoint = "sqlite3_config", CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_config_int(Config op, int value); ////[DllImport(SQLITE_DLL, EntryPoint = "sqlite3_config", CallingConvention = CallingConvention.Cdecl)] ////internal static extern SLError sqlite3_config_log(Config op, SQLiteLogCallback func, IntPtr pvUser); //[DllImport(SQLITE_DLL, EntryPoint = "sqlite3_db_config", CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_db_config_charptr(IntPtr db, ConfigDb op, IntPtr charPtr); //[DllImport(SQLITE_DLL, EntryPoint = "sqlite3_db_config", CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_db_config_int_refint(IntPtr db, ConfigDb op, int value, ref int result); //[DllImport(SQLITE_DLL, EntryPoint = "sqlite3_db_config", CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_db_config_intptr_two_ints(IntPtr db, ConfigDb op, IntPtr ptr, int int0, int int1); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_db_status(IntPtr db, SQLiteStatusOpsEnum op, ref int current, ref int highwater, int resetFlag); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern IntPtr sqlite3_rollback_hook(IntPtr db, SQLiteRollbackCallback func, IntPtr pvUser); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern IntPtr sqlite3_db_handle(IntPtr stmt); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_db_release_memory(IntPtr db); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern IntPtr sqlite3_db_filename(IntPtr db, byte[] dbName); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern int sqlite3_db_readonly(IntPtr db, IntPtr dbName); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern IntPtr sqlite3_next_stmt(IntPtr db, IntPtr stmt); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern SLError sqlite3_exec(IntPtr db, byte[] strSql, IntPtr pvCallback, IntPtr pvParam, byte** errMsg); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern int sqlite3_release_memory(int nBytes); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern int sqlite3_get_autocommit(IntPtr db); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_extended_result_codes(IntPtr db, int onoff); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern SLError sqlite3_errcode(IntPtr db); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_extended_errcode(IntPtr db); [DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] internal static extern byte* sqlite3_errstr(SLError rc); internal static string Errstr(SLError r) => Convert2.Utf8Decode(sqlite3_errstr(r)); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern void sqlite3_log(SLError iErrCode, byte[] zFormat); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_file_control(IntPtr db, byte[] zDbName, int op, IntPtr pArg); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern IntPtr sqlite3_backup_init(IntPtr destDb, byte[] zDestName, IntPtr sourceDb, byte[] zSourceName); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_backup_step(IntPtr backup, int nPage); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern int sqlite3_backup_remaining(IntPtr backup); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern int sqlite3_backup_pagecount(IntPtr backup); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_blob_close(IntPtr blob); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern int sqlite3_blob_bytes(IntPtr blob); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_blob_open(IntPtr db, byte[] dbName, byte[] tblName, byte[] colName, long rowId, int flags, ref IntPtr ptrBlob); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_blob_read(IntPtr blob, [MarshalAs(UnmanagedType.LPArray)] byte[] buffer, int count, int offset); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_blob_reopen(IntPtr blob, long rowId); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_blob_write(IntPtr blob, [MarshalAs(UnmanagedType.LPArray)] byte[] buffer, int count, int offset); //[DllImport(SQLITE_DLL, CallingConvention = CallingConvention.Cdecl)] //internal static extern SLError sqlite3_declare_vtab(IntPtr db, byte[] zSQL); } #endregion ================================================ FILE: Au/GUI/EnumUI.cs ================================================ using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; namespace Au.More; /// /// Add enum members to a menu or WPF control. /// public class EnumUI where TEnum : unmanaged, Enum { (TEnum e, object c, string text, string tt)[] _a; Selector _cb; bool _isFlags; /// /// Adds enum members to a menu as checkbox-items (if it's a [Flags] enum) or radio-items. /// /// /// Initial value. /// Enum members and their text/tooltip. Optional. Text can be: null, "text", "text|tooltip", "|tooltip". /// Sort by name. /// /// (m, KMod.Ctrl|KMod.Alt); //a [Flags] enum /// m.Separator(); /// var e = new EnumUI(m, DateTime.Today.DayOfWeek); //a non-[Flags] enum /// m.Show(); /// print.it(f.Result); /// print.it(e.Result); /// ]]> /// /// public EnumUI(popupMenu m, TEnum init = default, (TEnum value, string text)[] items = null, bool sort = false) { Not_.Null(m); _isFlags = typeof(TEnum).IsDefined(typeof(FlagsAttribute), false); bool cns = m.CheckDontClose; if (_isFlags) m.CheckDontClose = true; _InitArray(items, sort); for (int i = 0; i < _a.Length; i++) { var (e, _, text, tt) = _a[i]; var mi = _isFlags ? m.AddCheck(text, init.HasFlag(e)) : m.AddRadio(text, init.Equals(e)); mi.Tooltip = tt; _a[i].c = mi; } if (_isFlags) m.CheckDontClose = cns; } void _InitArray((TEnum value, string text)[] items, bool sort) { if (items != null) { _a = new (TEnum e, object c, string text, string tt)[items.Length]; for (int i = 0; i < _a.Length; i++) { var (v, text) = items[i]; _a[i].e = v; if (text != null) { int j = text.IndexOf('|'); if (j >= 0) { _a[i].tt = text[(j + 1)..]; text = j == 0 ? null : text[..j]; } } _a[i].text = text ?? v.ToString(); if (sort) _a = _a.OrderBy(o => o.text).ToArray(); } } else { IEnumerable e = Enum.GetValues(); if (_isFlags) e = e.Where(o => !EqualityComparer.Default.Equals(o, default)); var ee = e.Select(o => (o, (object)null, text: o.ToString(), (string)null)); if (sort) ee = ee.OrderBy(o => o.text); _a = ee.ToArray(); } } /// /// Adds members of a [Flags] enum as checkboxes to a WPF panel. /// /// , or . If Grid without columns, adds 2 columns. /// Initial value. /// Enum members and their text/tooltip. Optional. Text can be: null, "text", "text|tooltip", "|tooltip". /// Sort by name. /// /// With . /// (b.Panel, KMod.Ctrl|KMod.Alt); /// b.End(); /// ... /// print.it(e.Result); /// ]]> /// /// public EnumUI(Panel container, TEnum init = default, (TEnum value, string text)[] items = null, bool sort = false) { Not_.Null(container); _isFlags = true; _InitArray(items, sort); int nCol = 0; if (container is Grid g1) { nCol = g1.ColumnDefinitions.Count; if (nCol == 0) { nCol = 2; g1.AddColumns(0, -1); } } Thickness margin = container is StackPanel { Orientation: Orientation.Vertical } ? new(2) : new(2, 2, 12, 2); for (int i = 0, row = -1; i < _a.Length; i++) { var (e, _, text, tt) = _a[i]; var c = new CheckBox { Content = text, ToolTip = tt, IsChecked = init.HasFlag(e), Margin = margin, HorizontalAlignment = HorizontalAlignment.Stretch }; _a[i].c = c; container.Children.Add(c); if (nCol > 0) { int col = i % nCol; if (col == 0) { row++; ((Grid)container).RowDefinitions.Add(new() { Height = new() }); } Grid.SetColumn(c, col); Grid.SetRow(c, row); } } } /// /// Adds members of a non-[Flags] enum to a WPF control, for example . /// /// /// Initial value. /// Enum members and their text/tooltip. Optional. Text can be: null, "text", "text|tooltip", "|tooltip". /// Sort by name. /// /// (cb1); /// ... /// print.it(e.Result); /// ]]> /// public EnumUI(Selector container, TEnum init = default, (TEnum value, string text)[] items = null, bool sort = false) { Not_.Null(container); _cb = container; _InitArray(items, sort); int iSel = -1; for (int i = 0; i < _a.Length; i++) { var (e, _, text, tt) = _a[i]; ListBoxItem k = container is ComboBox ? new ComboBoxItem() : new ListBoxItem(); k.Content = text; k.ToolTip = tt; _cb.Items.Add(k); if (iSel < 0 && init.Equals(e)) iSel = i; } if (iSel >= 0) _cb.SelectedIndex = iSel; } /// /// Gets enum value from checked/selected menu items or WPF elements. /// public TEnum Result { get { TEnum r = default; if (_cb != null) { int i = _cb.SelectedIndex; if (i >= 0) r = _a[i].e; } else { for (int i = 0; i < _a.Length; i++) { var (e, c, _, _) = _a[i]; switch (c) { case PMItem mi: if (!mi.IsChecked) continue; break; case CheckBox ch: if (ch.IsChecked != true) continue; break; } if (_isFlags) { ExtMisc.SetFlag(ref r, e, true); } else { r = e; break; } } } return r; } } } ================================================ FILE: Au/GUI/dialog-static.cs ================================================ namespace Au; public partial class dialog { #region Show /// /// Shows dialog. /// /// Selected button id. /// Heading text. /// Message text. Can be string, or string with links like link text.", e => { print.it("link"); })]]>. /// /// List of button names or "id name". Examples: "OK|Cancel", "1 Yes|2 No", , ["1 One", "2 Two"]. /// Can contain common buttons (named OK, Yes, No, Retry, Cancel, Close) and/or custom buttons (any other names). /// This first in the list button will be focused (aka default button). /// More info: . /// /// /// /// Owner window. See . /// Text in expander control. Can be string, or string with links like link text.", e => { print.it("link"); })]]>. /// Text at the bottom of the dialog. Can be string, or string with links like link text.", e => { print.it("link"); })]]>. Icon can be specified like "i|Text", where i is: x error, ! warning, i info, v shield, a app. /// Title bar text. If omitted, null or "", uses . /// Can be used to add more controls and later get their values: checkbox, radio buttons, text input. /// X position in screen. Default - screen center. Examples: 10, ^10 (reverse), .5f (fraction). /// Y position in screen. Default - screen center. /// See . Examples: screen.ofMouse, screen.at.left(). /// If not 0, after this time (seconds) auto-close the dialog and return . /// /// Tip: Use named arguments. Example: dialog.show("Text", icon: DIcon.Info, title: "Title") . /// /// This function allows you to use many dialog features, but not all. Alternatively you can create a class instance, set properties and call . Example in class help. /// /// /// /// /// /// /// Failed to show dialog. Unlikely. public static int show( string text1 = null, DText text2 = null, Strings buttons = default, DFlags flags = 0, DIcon icon = 0, AnyWnd owner = default, DText expandedText = null, DText footer = null, string title = null, DControls controls = null, Coord x = default, Coord y = default, screen screen = default, int secondsTimeout = 0 ) { var d = new dialog(text1, text2, buttons, flags, icon, owner, expandedText, footer, title, controls, x, y, screen, secondsTimeout); return d.ShowDialog(); } /// /// Shows dialog with icon. /// /// Calls . /// /// public static int showInfo(string text1 = null, DText text2 = null, Strings buttons = default, DFlags flags = 0, AnyWnd owner = default, DText expandedText = null, string title = null, int secondsTimeout = 0) => show(text1, text2, buttons, flags, DIcon.Info, owner, expandedText, title: title, secondsTimeout: secondsTimeout); /// /// Shows dialog with icon. /// /// Calls . /// /// public static int showWarning(string text1 = null, DText text2 = null, Strings buttons = default, DFlags flags = 0, AnyWnd owner = default, DText expandedText = null, string title = null, int secondsTimeout = 0) => show(text1, text2, buttons, flags, DIcon.Warning, owner, expandedText, title: title, secondsTimeout: secondsTimeout); /// /// Shows dialog with icon. /// /// Calls . /// /// public static int showError(string text1 = null, DText text2 = null, Strings buttons = default, DFlags flags = 0, AnyWnd owner = default, DText expandedText = null, string title = null, int secondsTimeout = 0) => show(text1, text2, buttons, flags, DIcon.Error, owner, expandedText, title: title, secondsTimeout: secondsTimeout); /// /// Shows dialog with OK and Cancel buttons. /// /// true if selected OK. /// Calls . /// /// public static bool showOkCancel(string text1 = null, DText text2 = null, DFlags flags = 0, DIcon icon = 0, AnyWnd owner = default, DText expandedText = null, string title = null, int secondsTimeout = 0) => 1 == show(text1, text2, "OK|Cancel", flags, icon, owner, expandedText, title: title, secondsTimeout: secondsTimeout); /// /// Shows dialog with Yes and No buttons. /// /// true if selected Yes. /// Calls . /// /// public static bool showYesNo(string text1 = null, DText text2 = null, DFlags flags = 0, DIcon icon = 0, AnyWnd owner = default, DText expandedText = null, string title = null, int secondsTimeout = 0) => 1 == show(text1, text2, "Yes|No", flags, icon, owner, expandedText, title: title, secondsTimeout: secondsTimeout); #endregion Show #region ShowInput /// /// Shows dialog with a text edit field and gets that text. /// /// true if selected OK (or a custom button with id 1). /// Variable that receives the text. /// Heading text. /// Message test (above the edit field). /// Edit field type. It can be simple text (default), multiline, number, password or combo box. /// Initial edit field text. /// Combo box items used when editType is . /// /// Owner window. See . /// Text in expander control. /// Text at the bottom of the dialog. Icon can be specified like "i|Text", where i is: x error, ! warning, i info, v shield, a app. /// Title bar text. If omitted, null or "", uses . /// Can be used to add more controls and later get their values: checkbox, radio buttons. /// X position in screen. Default - screen center. Examples: 10, ^10 (reverse), .5f (fraction). /// Y position in screen. Default - screen center. /// See . Examples: screen.ofMouse, screen.index(1). /// If not 0, after this time (seconds) auto-close the dialog and return . /// /// Buttons. List of names or "id name". Example: "1 OK|2 Cancel|10 Browse...". See . /// Note: this function returns true only when clicked button with id 1. /// Usually custom buttons are used with onButtonClick function, which for example can get button id or disable closing the dialog. /// /// A button-clicked event handler function. See examples. /// /// This function allows you to use many dialog features, but not all. Alternatively you can create a class instance, call or use the controls parameter, set other properties and call . /// /// /// Simple. /// /// /// With checkbox. /// /// /// With onButtonClick function. /// r = e.Button); /// print.it(r); /// /// if(!dialog.showInput(out string s, "Example", flags: DFlags.CommandLinks, buttons: "OK|Cancel|10 Set text", onButtonClick: e => { /// if(e.Button == 10) { e.EditText = "text"; e.DontCloseDialog = true; } /// })) return; /// /// if(!dialog.showInput(out string s2, "Example", "Try to click OK while text is empty.", onButtonClick: e => { /// if(e.Button == 1 && e.EditText.NE()) { /// dialog.show("Text cannot be empty.", owner: e.hwnd); /// e.d.EditControl.Focus(); /// e.DontCloseDialog = true; /// } /// })) return; /// ]]> /// /// Failed to show dialog. public static bool showInput(out string s, string text1 = null, DText text2 = null, DEdit editType = DEdit.Text, string editText = null, Strings comboItems = default, DFlags flags = 0, AnyWnd owner = default, DText expandedText = null, DText footer = null, string title = null, DControls controls = null, Coord x = default, Coord y = default, screen screen = default, int secondsTimeout = 0, string buttons = "1 OK|2 Cancel", Action onButtonClick = null ) { if (buttons.NE()) buttons = "1 OK|2 Cancel"; var d = new dialog(text1, text2, buttons, flags, 0, owner, expandedText, footer, title, controls, x, y, screen, secondsTimeout); d.Edit(editType != 0 ? editType : DEdit.Text, editText, comboItems); if (onButtonClick != null) d.ButtonClicked += onButtonClick; bool r = 1 == d.ShowDialog(); s = r ? d._controls.EditText : null; return r; } /// /// Shows dialog with a number edit field and gets that number. /// /// true if selected OK. /// Variable that receives the number. /// Heading text. /// Message text (above the edit field). /// Initial edit field text. /// /// Owner window. See . /// /// Calls and converts string to int. /// /// /// /// /// Failed to show dialog. public static bool showInputNumber(out int i, string text1 = null, DText text2 = null, int? editText = null, DFlags flags = 0, AnyWnd owner = default ) { i = 0; if (!showInput(out string s, text1, text2, DEdit.Number, editText?.ToString(), default, flags, owner)) return false; i = s.ToInt(); return true; } #endregion ShowInput #region ShowList /// /// Shows dialog with a list of command-link buttons, and returns 1-based button index or 0. /// /// 1-based index of the selected button. Returns 0 if clicked the X (close window) button or pressed Esc. /// List items (buttons). Can be like "One|Two|Three" or new("One", "Two", "Three") or string array or List. See . /// Heading text. /// Message text. /// /// Owner window. See . /// Text in expander control. /// Text at the bottom of the dialog. Icon can be specified like "i|Text", where i is: x error, ! warning, i info, v shield, a app. /// Title bar text. If omitted, null or "", uses . /// Can be used to add more controls and later get their values: checkbox, radio buttons, text input. /// X position in screen. Default - screen center. Examples: 10, ^10 (reverse), .5f (fraction). /// Y position in screen. Default - screen center. /// See . Examples: screen.ofMouse, screen.index(1). /// If not 0, after this time (seconds) auto-close the dialog and return . /// /// This function allows you to use most of the dialog features, but not all. Alternatively you can create a class instance, set properties and call . Example in class help. /// /// /// /// /// Failed to show dialog. /// public static int showList( Strings list, string text1 = null, DText text2 = null, DFlags flags = 0, AnyWnd owner = default, DText expandedText = null, DText footer = null, string title = null, DControls controls = null, Coord x = default, Coord y = default, screen screen = default, int secondsTimeout = 0 ) { return new dialog(text1, text2, default, flags | DFlags.XCancel | DFlags.ExpandDown, 0, owner, expandedText, footer, title, controls, x, y, screen, secondsTimeout) .ButtonsList(list) .ShowDialog(); } #endregion ShowList #region ShowProgress /// /// Shows dialog with progress bar. /// Creates dialog in new thread and returns without waiting until it is closed. /// /// Variable that can be used to communicate with the dialog using these methods and properties: , , (when closed), (when closed), , ; through the Send property you can set progress, modify controls and close the dialog (see example). /// Let the progress bar animate without indicating a percent of work done. /// /// This function allows you to use most of the dialog features, but not all. Alternatively you can create a class instance, set properties and call . /// /// /// /// /// public static dialog showProgress(bool marquee, string text1 = null, DText text2 = null, string buttons = "0 Cancel", DFlags flags = 0, AnyWnd owner = default, DText expandedText = null, DText footer = null, string title = null, DControls controls = null, Coord x = default, Coord y = default, screen screen = default, int secondsTimeout = 0 ) { if (buttons.NE()) buttons = "0 Cancel"; var d = new dialog(text1, text2, buttons, flags, 0, owner, expandedText, footer, title, controls, x, y, screen, secondsTimeout); d.Progress(true, marquee); d.ShowDialogNoWait(); return d; } #endregion ShowProgress #region ShowNoWait /// /// Shows dialog like but does not wait. /// Creates dialog in other thread and returns without waiting until it is closed. /// /// Variable that can be used to communicate with the dialog using these methods and properties: , , (when closed), (when closed), , ; through the Send property you can modify controls and close the dialog (see example). /// /// This function allows you to use most of the dialog features, but not all. Alternatively you can create a class instance, set properties and call . /// /// /// /// /// public static dialog showNoWait( string text1 = null, DText text2 = null, Strings buttons = default, DFlags flags = 0, DIcon icon = 0, AnyWnd owner = default, DText expandedText = null, DText footer = null, string title = null, DControls controls = null, Coord x = default, Coord y = default, screen screen = default, int secondsTimeout = 0 ) { var d = new dialog(text1, text2, buttons, flags, icon, owner, expandedText, footer, title, controls, x, y, screen, secondsTimeout); d.ShowDialogNoWait(); return d; } #endregion ShowNoWait } ================================================ FILE: Au/GUI/dialog-types.cs ================================================ #pragma warning disable 649 //unused fields in API structs namespace Au.Types { /// /// Text for functions. Supports links. Has implicit conversion from string. /// /// Text. Links can be specified like link text."]]>. /// Link click handlers like e => { print.it("link"); }. public record class DText(string text, params Action[] links) { /// /// Implicit conversion from string. /// /// null if the string is null. public static implicit operator DText(string s) => s == null ? null : new(s, null); } #pragma warning disable 1591 //missing XML documentation /// /// Standard icons for functions. /// public enum DIcon { Warning = 0xffff, Error = 0xfffe, Info = 0xfffd, Shield = 0xfffc, //these are undocumented but used in .NET TaskDialogStandardIcon. But why need? //ShieldBlueBar = ushort.MaxValue - 4, //ShieldGrayBar = ushort.MaxValue - 8, //ShieldWarningYellowBar = ushort.MaxValue - 5, //ShieldErrorRedBar = ushort.MaxValue - 6, //ShieldSuccessGreenBar = ushort.MaxValue - 7, /// /// Use IDI_APPLICATION icon from unmanaged resources of this program file or main assembly. /// If there are no icons - default program icon. /// C# compilers add app icon with this id. The value is = IDI_APPLICATION (32512). /// If this program file contains multiple native icons in range DIcon.App to 0xf000, you can specify them like DIcon.App+1. /// App = Api.IDI_APPLICATION } /// /// Text edit field type for , , etc. /// public enum DEdit { None, Text, Multiline, Password, Number, Combo } #pragma warning restore 1591 //missing XML documentation /// /// Flags for functions. /// [Flags] public enum DFlags { /// /// Display custom buttons as a column of command-links, not as a row of classic buttons. /// Command links can have multi-line text. The first line has bigger font. /// More info about custom buttons: . /// CommandLinks = 1, /// /// Show expanded text in footer. /// ExpandDown = 1 << 1, /// /// Call with width 700. /// Wider = 1 << 2, /// /// Allow to cancel even if there is no Cancel button. /// It adds X (Close) button to the title bar, and also allows to close the dialog with the Esc key. /// When the dialog is closed with the X button or Esc, the returned result button id is 0 if there is no Cancel button; else the same as when clicked the Cancel button. /// XCancel = 1 << 3, /// /// Show the dialog in the center of the owner window. /// CenterOwner = 1 << 4, /// /// Show the dialog at the mouse position. /// CenterMouse = 1 << 5, /// /// x y are raw coordinates relative to the primary screen. /// More info: , . /// RawXY = 1 << 6, /// /// Add Minimize button to the title bar. /// This flag is ignored if owner window specified. /// MinimizeButton = 1 << 7, /// /// Make the dialog a topmost window (always on top of other windows), regardless of etc. /// /// If neither Topmost nor NoTopmost are set, makes topmost if both these are true: no owner window, is true (default). /// Topmost = 1 << 8, /// /// Don't make the dialog a topmost window, regardless of etc. /// NoTopmost = 1 << 9, //NoTaskbarButton = , //not so useful //NeverActivate = , //don't know how to implement. TDF_NO_SET_FOREGROUND does not work. LockSetForegroundWindow does not work if we can activate windows. HCBT_ACTIVATE can prevent activating but does not prevent deactivating. //AlwaysActivate = , //Don't use. Always allow. Because after AllowActivate (which is also used by Activate etc) always activates dialogs regardless of anything. As well as in uiAccess process. } /// /// Used with and similar functions to add more controls and get their final values. /// public class DControls { /// /// If not null, adds checkbox with this text. /// public string Checkbox { get; set; } /// /// Sets initial and gets final checkbox value (true if checked). /// public bool IsChecked { get; set; } /// /// Adds radio buttons. /// A list of strings "id text" separated by |, like "1 One|2 Two|3 Three". /// public Strings RadioButtons { get; set; } /// /// Sets initial and gets final checked radio button. It is button id (as specified in ), not index. /// See . /// public int RadioId { get; set; } /// /// Adds a text edit control. /// Note: then the dialog cannot have a progress bar. /// public DEdit EditType { get; set; } /// /// Sets initial and gets final text edit control value. /// public string EditText { get; set; } /// /// Sets combo box list items used when is . /// public Strings ComboItems { get; set; } } /// /// Arguments for event handlers. /// /// The dialog. /// The dialog window. /// See task dialog notifications. /// See task dialog notifications. /// In a event handler - button id. /// In a event handler - timer time in milliseconds. The handler can set returnValue = 1 to reset this. /// In an event handler - the href attribute. /// /// To return a non-zero value from the callback function, assign the value to the returnValue field. /// More info: TaskDialogCallbackProc. /// public record class DEventArgs(dialog d, wnd hwnd, DNative.TDN message, nint wParam, int Button, int TimerTimeMS, string LinkHref) { /// public int returnValue; /// /// Your event handler function can use this to prevent closing the dialog. /// public bool DontCloseDialog { set { returnValue = value ? 1 : 0; } } /// /// Gets or sets edit field text. /// public string EditText { get => d.EditControl.ControlText; set { d.EditControl.SetText(value); } } } /// /// Can be used through , to interact with dialog while it is open. /// /// /// Example (in an event handler): e.d.Close(); /// public class DSend { volatile dialog _tdo; internal DSend(dialog tdo) { _tdo = tdo; } internal void Clear_() { _tdo = null; } /// /// Sends a message to the dialog. /// /// /// Call this method while the dialog is open, eg in an event handler. /// Example (in an event handler): e.d.Send.Message(DNative.TDM.CLICK_VERIFICATION, 1); /// Also there are several other functions to send some messages: change text, close dialog, enable/disable buttons, update progress. /// Reference: task dialog messages. /// NAVIGATE_PAGE not supported. /// public int Message(DNative.TDM message, nint wParam = 0, nint lParam = 0) { return _tdo?.SendMessage_(message, wParam, lParam) ?? 0; } void _SetText(bool resizeDialog, DNative.TDE partId, string text) { _tdo?.SetText_(resizeDialog, partId, text); } /// /// Changes the main big-font text. /// /// /// Call this method while the dialog is open, eg in an event handler. /// public void ChangeText1(string text, bool resizeDialog) { _SetText(resizeDialog, DNative.TDE.MAIN_INSTRUCTION, text); } /// /// Changes the main small-font text. /// /// /// Call this method while the dialog is open, eg in an event handler. /// public void ChangeText2(string text, bool resizeDialog) { _SetText(resizeDialog, DNative.TDE.CONTENT, text); } /// /// Changes the footer text. /// /// /// Call this method while the dialog is open, eg in an event handler. /// public void ChangeFooterText(string text, bool resizeDialog) { _SetText(resizeDialog, DNative.TDE.FOOTER, text); } /// /// Changes the expanded area text. /// /// /// Call this method while the dialog is open, eg in an event handler. /// public void ChangeExpandedText(string text, bool resizeDialog) { _SetText(resizeDialog, DNative.TDE.EXPANDED_INFORMATION, text); } #if false //currently not implemented /// /// Applies new properties to the dialog while it is already open. /// Call this method while the dialog is open, eg in an event handler, after setting new properties. /// Sends message DNative.TDM.NAVIGATE_PAGE. /// public void Reconstruct() { var td = _tdo; if(td == null) return; _ApiSendMessageTASKDIALOGCONFIG(_dlg, (uint)DNative.TDM.NAVIGATE_PAGE, 0, ref td._c); } [DllImport("user32.dll", EntryPoint = "SendMessageW")] static extern nint _ApiSendMessageTASKDIALOGCONFIG(wnd hWnd, uint msg, nint wParam, in TASKDIALOGCONFIG c); #endif /// /// Clicks a button. Normally it closes the dialog. /// /// A button id or some other number that will be returned by . /// /// Call this method while the dialog is open, eg in an event handler. /// Sends message . /// public bool Close(int buttonId = 0) { return 0 != Message(DNative.TDM.CLICK_BUTTON, buttonId); } /// /// Enables or disables a button. /// /// /// Call this method while the dialog is open, eg in an event handler. /// Example: d.Created += e => { e.d.Send.EnableButton(4, false); }; /// Sends message . /// public void EnableButton(int buttonId, bool enable) { Message(DNative.TDM.ENABLE_BUTTON, buttonId, enable ? 1 : 0); } /// /// Sets progress bar value, 0 to 100. /// /// /// Call this method while the dialog is open, eg in an event handler. /// Sends message . /// public int Progress(int percent) { if (percent < 100) Message(DNative.TDM.SET_PROGRESS_BAR_POS, percent + 1); //workaround for the progress bar control lag. https://stackoverflow.com/questions/5332616/disabling-net-progressbar-animation-when-changing-value return Message(DNative.TDM.SET_PROGRESS_BAR_POS, percent); } } #region public WinAPI #pragma warning disable 1591 //missing XML documentation /// /// Rarely used constants for Windows API used by . /// /// /// Constants are in enums. Enum name is constant prefix. Enum members are without prefix. For example for TDM_CLICK_BUTTON use DNative.TDM.CLICK_BUTTON. /// public static class DNative { /// /// Messages that your event handler can send to the dialog. /// public enum TDM { NAVIGATE_PAGE = Api.WM_USER + 101, CLICK_BUTTON = Api.WM_USER + 102, // wParam = button id SET_MARQUEE_PROGRESS_BAR = Api.WM_USER + 103, // wParam = 0 (nonMarque) wParam != 0 (Marquee) SET_PROGRESS_BAR_STATE = Api.WM_USER + 104, // wParam = new progress state (0, 1 or 2) SET_PROGRESS_BAR_RANGE = Api.WM_USER + 105, // lParam = Math2.MakeLparam(min, max) SET_PROGRESS_BAR_POS = Api.WM_USER + 106, // wParam = new position SET_PROGRESS_BAR_MARQUEE = Api.WM_USER + 107, // wParam = 0 (stop marquee), wParam != 0 (start marquee), lParam = speed (milliseconds between repaints) SET_ELEMENT_TEXT = Api.WM_USER + 108, // wParam = element (enum DNative.TDE), lParam = new element text (string) CLICK_RADIO_BUTTON = Api.WM_USER + 110, // wParam = radio button id ENABLE_BUTTON = Api.WM_USER + 111, // wParam = button id, lParam = 0 (disable), lParam != 0 (enable) ENABLE_RADIO_BUTTON = Api.WM_USER + 112, // wParam = radio button id, lParam = 0 (disable), lParam != 0 (enable) CLICK_VERIFICATION = Api.WM_USER + 113, // wParam = 0 (unchecked), 1 (checked), lParam = 1 (set key focus) UPDATE_ELEMENT_TEXT = Api.WM_USER + 114, // wParam = element (enum DNative.TDE), lParam = new element text (string) SET_BUTTON_ELEVATION_REQUIRED_STATE = Api.WM_USER + 115, // wParam = button id, lParam = 0 (elevation not required), lParam != 0 (elevation required) UPDATE_ICON = Api.WM_USER + 116 // wParam = icon element (enum DNative.TDIE), lParam = new icon (icon handle or DIcon) } /// /// Notification messages that your event handler receives. /// public enum TDN : uint { CREATED = 0, NAVIGATED = 1, BUTTON_CLICKED = 2, HYPERLINK_CLICKED = 3, TIMER = 4, DESTROYED = 5, RADIO_BUTTON_CLICKED = 6, DIALOG_CONSTRUCTED = 7, VERIFICATION_CLICKED = 8, HELP = 9, EXPANDO_BUTTON_CLICKED = 10 } /// /// Constants for and messages used with . /// public enum TDE { CONTENT, EXPANDED_INFORMATION, FOOTER, MAIN_INSTRUCTION } /// /// Constants for message used with . /// public enum TDIE { ICON_MAIN, ICON_FOOTER } } #pragma warning restore 1591 //missing XML documentation #endregion public WinAPI } namespace Au { public partial class dialog { #region private WinAPI delegate int _TaskDialogIndirectDelegate(in TASKDIALOGCONFIG c, out int pnButton, out int pnRadioButton, out int pChecked); static readonly _TaskDialogIndirectDelegate TaskDialogIndirect = _GetTaskDialogIndirect(); static _TaskDialogIndirectDelegate _GetTaskDialogIndirect() { //Activate manifest that tells to use comctl32.dll version 6. The API is unavailable in version 5. //Need this if the host app does not have such manifest, eg if uses the default manifest added by Visual Studio. using (ActCtx_.Activate()) { //don't use DllImport, because it uses v5 comctl32.dll if it is already loaded. Api.GetDelegate(out _TaskDialogIndirectDelegate R, "comctl32.dll", "TaskDialogIndirect"); return R; } } //TASKDIALOGCONFIG flags. [Flags] enum _TDF { ENABLE_HYPERLINKS = 0x0001, USE_HICON_MAIN = 0x0002, USE_HICON_FOOTER = 0x0004, ALLOW_DIALOG_CANCELLATION = 0x0008, USE_COMMAND_LINKS = 0x0010, USE_COMMAND_LINKS_NO_ICON = 0x0020, EXPAND_FOOTER_AREA = 0x0040, EXPANDED_BY_DEFAULT = 0x0080, VERIFICATION_FLAG_CHECKED = 0x0100, SHOW_PROGRESS_BAR = 0x0200, SHOW_MARQUEE_PROGRESS_BAR = 0x0400, CALLBACK_TIMER = 0x0800, POSITION_RELATIVE_TO_WINDOW = 0x1000, RTL_LAYOUT = 0x2000, NO_DEFAULT_RADIO_BUTTON = 0x4000, CAN_BE_MINIMIZED = 0x8000, //NO_SET_FOREGROUND = 0x00010000, //Win8, does not work SIZE_TO_CONTENT = 0x1000000, } //TASKDIALOGCONFIG buttons. [Flags] enum _TDCBF { OK = 1, Yes = 2, No = 4, Cancel = 8, Retry = 0x10, Close = 0x20, } [StructLayout(LayoutKind.Sequential, Pack = 1)] unsafe struct TASKDIALOG_BUTTON { public int id; public char* text; } [StructLayout(LayoutKind.Sequential, Pack = 1)] unsafe struct TASKDIALOGCONFIG { public int cbSize; public wnd hwndParent; public IntPtr hInstance; public _TDF dwFlags; public _TDCBF dwCommonButtons; public string pszWindowTitle; public IntPtr hMainIcon; public string pszMainInstruction; public string pszContent; public int cButtons; public TASKDIALOG_BUTTON* pButtons; public int nDefaultButton; public int cRadioButtons; public TASKDIALOG_BUTTON* pRadioButtons; public int nDefaultRadioButton; public string pszVerificationText; public string pszExpandedInformation; public string pszExpandedControlText; public string pszCollapsedControlText; public IntPtr hFooterIcon; public string pszFooter; public TaskDialogCallbackProc pfCallback; public IntPtr lpCallbackData; public int cxWidth; } delegate int TaskDialogCallbackProc(wnd hwnd, DNative.TDN notification, nint wParam, nint lParam, IntPtr data); #endregion private WinAPI } } ================================================ FILE: Au/GUI/dialog-x-obsolete.cs ================================================ //info: the "x" in filename is for DocFX to correctly resolve links (changes file processing order). #if !DEBUG && NET9_0_OR_GREATER namespace Au; public partial class dialog { /// This overload is obsolete. For text with links now use instead of string + onLinkClick. /// [EditorBrowsable(EditorBrowsableState.Never)] [OverloadResolutionPriority(-1)] public dialog( string text1 = null, string text2 = null, Strings buttons = default, DFlags flags = 0, DIcon icon = 0, AnyWnd owner = default, string expandedText = null, string footer = null, string title = null, DControls controls = null, int defaultButton = 0, Coord x = default, Coord y = default, screen screen = default, int secondsTimeout = 0, Action onLinkClick = null ) : this(text1, text2, buttons, flags, icon, owner, expandedText, footer, title, controls, x, y, screen, secondsTimeout) { if (defaultButton != 0) Default(defaultButton); if (onLinkClick != null) HyperlinkClicked += onLinkClick; } /// This overload is obsolete. For text with links now use instead of string + onLinkClick. /// [EditorBrowsable(EditorBrowsableState.Never)] [OverloadResolutionPriority(-1)] public static int show( string text1 = null, string text2 = null, Strings buttons = default, DFlags flags = 0, DIcon icon = 0, AnyWnd owner = default, string expandedText = null, string footer = null, string title = null, DControls controls = null, int defaultButton = 0, Coord x = default, Coord y = default, screen screen = default, int secondsTimeout = 0, Action onLinkClick = null ) { var d = new dialog(text1, text2, buttons, flags, icon, owner, expandedText, footer, title, controls, defaultButton, x, y, screen, secondsTimeout, onLinkClick); return d.ShowDialog(); } /// This overload is obsolete. For text with links now use instead of string + onLinkClick. /// [EditorBrowsable(EditorBrowsableState.Never)] [OverloadResolutionPriority(-1)] public static bool showInput(out string s, string text1 = null, string text2 = null, DEdit editType = DEdit.Text, string editText = null, Strings comboItems = default, DFlags flags = 0, AnyWnd owner = default, string expandedText = null, string footer = null, string title = null, DControls controls = null, Coord x = default, Coord y = default, screen screen = default, int secondsTimeout = 0, Action onLinkClick = null, string buttons = "1 OK|2 Cancel", Action onButtonClick = null ) { if (buttons.NE()) buttons = "1 OK|2 Cancel"; var d = new dialog(text1, text2, buttons, flags, 0, owner, expandedText, footer, title, controls, 0, x, y, screen, secondsTimeout, onLinkClick); d.Edit(editType != 0 ? editType : DEdit.Text, editText, comboItems); if (onButtonClick != null) d.ButtonClicked += onButtonClick; bool r = 1 == d.ShowDialog(); s = r ? d._controls.EditText : null; return r; } /// This overload is obsolete. For text with links now use instead of string + onLinkClick. /// [EditorBrowsable(EditorBrowsableState.Never)] [OverloadResolutionPriority(-1)] public static int showList( Strings list, string text1 = null, string text2 = null, DFlags flags = 0, AnyWnd owner = default, string expandedText = null, string footer = null, string title = null, DControls controls = null, int defaultButton = 0, Coord x = default, Coord y = default, screen screen = default, int secondsTimeout = 0, Action onLinkClick = null ) { var d = new dialog(text1, text2, default, flags | DFlags.XCancel | DFlags.ExpandDown, 0, owner, expandedText, footer, title, controls, defaultButton, x, y, screen, secondsTimeout, onLinkClick); d.ButtonsList(list); return d.ShowDialog(); } /// This overload is obsolete. For text with links now use instead of string + onLinkClick. /// [EditorBrowsable(EditorBrowsableState.Never)] [OverloadResolutionPriority(-1)] public static dialog showProgress(bool marquee, string text1 = null, string text2 = null, string buttons = "0 Cancel", DFlags flags = 0, AnyWnd owner = default, string expandedText = null, string footer = null, string title = null, DControls controls = null, Coord x = default, Coord y = default, screen screen = default, int secondsTimeout = 0, Action onLinkClick = null ) { if (buttons.NE()) buttons = "0 Cancel"; var d = new dialog(text1, text2, buttons, flags, 0, owner, expandedText, footer, title, controls, 0, x, y, screen, secondsTimeout, onLinkClick); d.Progress(true, marquee); d.ShowDialogNoWait(); return d; } /// This overload is obsolete. For text with links now use instead of string + onLinkClick. /// [EditorBrowsable(EditorBrowsableState.Never)] [OverloadResolutionPriority(-1)] public static dialog showNoWait( string text1 = null, string text2 = null, Strings buttons = default, DFlags flags = 0, DIcon icon = 0, AnyWnd owner = default, string expandedText = null, string footer = null, string title = null, DControls controls = null, int defaultButton = 0, Coord x = default, Coord y = default, screen screen = default, int secondsTimeout = 0, Action onLinkClick = null ) { var d = new dialog(text1, text2, buttons, flags, icon, owner, expandedText, footer, title, controls, defaultButton, x, y, screen, secondsTimeout, onLinkClick); d.ShowDialogNoWait(); return d; } /// [EditorBrowsable(EditorBrowsableState.Never)] public void SetTitleBarText(string title) { Title(title); } /// /// Sets text. /// /// Heading text. /// Message text. [EditorBrowsable(EditorBrowsableState.Never)] public void SetText(string text1 = null, string text2 = null) { _c.pszMainInstruction = text1; _c.pszContent = text2; } /// [EditorBrowsable(EditorBrowsableState.Never)] public void SetIcon(DIcon icon) => Icon(icon); /// [EditorBrowsable(EditorBrowsableState.Never)] public void SetIcon(object icon) => Icon(icon); /// [EditorBrowsable(EditorBrowsableState.Never)] public void SetButtons(Strings buttons, bool asCommandLinks = false, Strings customButtons = default) { Buttons(buttons, asCommandLinks).ButtonsList(customButtons, asCommandLinks); } /// /// Specifies which button responds to the Enter key. /// If 0 or not set, auto-selects. /// /// Button id. [EditorBrowsable(EditorBrowsableState.Never)] public int DefaultButton { set { Default(value); } } /// [EditorBrowsable(EditorBrowsableState.Never)] public void SetRadioButtons(Strings buttons, int defaultId = 0) => RadioButtons(buttons, defaultId); /// [EditorBrowsable(EditorBrowsableState.Never)] public void SetCheckbox(string text, bool check = false) => Checkbox(text, check); /// [EditorBrowsable(EditorBrowsableState.Never)] public void SetExpandedText(string text, bool showInFooter = false) => ExpandedText(text, showInFooter); /// [EditorBrowsable(EditorBrowsableState.Never)] public void SetExpandControl(bool defaultExpanded, string collapsedText = null, string expandedText = null) => Expander(defaultExpanded, collapsedText, expandedText); /// /// Adds text and common icon at the bottom of the dialog. /// /// Text, optionally preceded by an icon character and |, like "i|Text". Icons: x error, ! warning, i info, v shield, a app. [EditorBrowsable(EditorBrowsableState.Never)] public void SetFooter(string text) => _SetFooter(text); /// /// Adds text and common icon at the bottom of the dialog. /// /// Text. /// [EditorBrowsable(EditorBrowsableState.Never)] public void SetFooter(string text, DIcon icon) => FooterText(text).FooterIcon(icon); /// /// Adds text and custom icon at the bottom of the dialog. /// /// [EditorBrowsable(EditorBrowsableState.Never)] public void SetFooter(string text, object icon) => FooterText(text).FooterIcon(icon); /// [EditorBrowsable(EditorBrowsableState.Never)] public void SetEditControl(DEdit editType, string editText = null, Strings comboItems = default) => Edit(editType, editText, comboItems); /// /// Makes the dialog wider. /// [EditorBrowsable(EditorBrowsableState.Never)] public int Width { set { _c.cxWidth = value / 2; } } /// [EditorBrowsable(EditorBrowsableState.Never)] public void SetOwnerWindow(AnyWnd owner, bool ownerCenter = false, bool dontDisable = false) => OwnerWindow(owner, ownerCenter, dontDisable); /// [EditorBrowsable(EditorBrowsableState.Never)] public void SetXY(Coord x, Coord y, bool rawXY = false) => XY(x, y, rawXY); /// /// Sets a timeout to close the dialog after closeAfterS seconds. Then returns . /// [EditorBrowsable(EditorBrowsableState.Never)] public void SetTimeout(int closeAfterS, string timeoutActionText = null, bool noInfo = false) => CloseAfter(closeAfterS, timeoutActionText, noInfo); } #endif ================================================ FILE: Au/GUI/dialog.cs ================================================ //rejected: by default show dialog in screen of mouse, like with dialog.options.defaultScreen = screen.ofMouse;. // Some Windows etc dialogs do it, and for me it's probably better. Eg Explorer's Properties even is at mouse position (top-left corner). //rejected: dialog.showCheckboxes. See Cookbook > Dialog - enum check-list, select. namespace Au; /// /// Standard dialogs to show information or get user input. /// /// /// You can use static functions like (less code) or create class instances. /// /// Uses task dialog API TaskDialogIndirect. /// /// Cannot be used in services. Instead use with option ServiceNotification or DefaultDesktopOnly, or API MessageBox with corresponding flags, or API WTSSendMessage. /// /// /// Simple examples. /// /// /// This example creates a class instance, sets properties, shows dialog, uses events, uses result. /// { print.it(e.Button); if(e.Button == 4) e.DontCloseDialog = true; }; /// d.Progress(); /// d.Timer += e => { e.d.Send.Progress(e.TimerTimeMS / 100); }; /// var r = d.ShowDialog(); /// print.it(r, d.Controls.IsChecked, d.Controls.RadioId); /// switch(r) { case 1: print.it("OK"); break; case dialog.Timeout: print.it("timeout"); break; } /// ]]> /// public partial class dialog { #region static options /// /// Default options used by class functions. /// public static class options { /// /// Default title bar text. /// Default value - . In exe it is exe file name like "Example.exe". /// public static string defaultTitle { get => _defaultTitle ?? script.name; set { _defaultTitle = value; } } static string _defaultTitle; /// /// Right-to-left layout. /// public static bool rtlLayout { get; set; } /// /// If there is no owner window, let the dialog be always on top of most other windows. /// Default true. /// /// public static bool topmostIfNoOwnerWindow { get; set; } = true; /// /// Show dialogs on this screen when screen is not explicitly specified ( or parameter screen) and there is no owner window. /// The must be lazy or empty. /// /// with Handle. Must be lazy or empty. /// /// /// public static screen defaultScreen { get => _defaultScreen; set => _defaultScreen = value.ThrowIfWithHandle_; } static screen _defaultScreen; /// /// If icon not specified, use . /// public static bool useAppIcon { get; set; } /// /// If owner window not specified (see ), use the active or top window of current thread as owner window (disable it, etc). /// public static bool autoOwnerWindow { get; set; } /// /// Timeout text format string. See . /// /// /// Default: "{0} s until this dialog closes, unless clicked.\nTimeout action: {1}.". /// Use placeholder {0} for seconds (in the first line) and {1} for timeout action (in the second line). /// public static string timeoutTextFormat { get; set; } = c_defaultTimeoutTextFormat; internal const string c_defaultTimeoutTextFormat = "{0} s until this dialog closes, unless clicked.\nTimeout action: {1}."; } #endregion static options TASKDIALOGCONFIG _c; DFlags _flags; dialog(DFlags flags) { _c.cbSize = Api.SizeOf(_c); _flags = flags; RtlLayout = options.rtlLayout; } /// /// Initializes a new instance and sets main properties. /// /// public dialog( string text1 = null, DText text2 = null, Strings buttons = default, DFlags flags = 0, DIcon icon = 0, AnyWnd owner = default, DText expandedText = null, DText footer = null, string title = null, DControls controls = null, Coord x = default, Coord y = default, screen screen = default, int secondsTimeout = 0 ) : this(flags) { Text1(text1); Text2(text2); Icon(icon); Buttons(buttons, 0 != (flags & DFlags.CommandLinks)); if (controls != null) { _controls = controls; if (controls.Checkbox != null) Checkbox(controls.Checkbox, controls.IsChecked); if (controls.RadioButtons.Value != null) RadioButtons(controls.RadioButtons, controls.RadioId); } OwnerWindow(owner, 0 != (flags & DFlags.CenterOwner)); XY(x, y, 0 != (flags & DFlags.RawXY)); InScreen(screen); CloseAfter(secondsTimeout); ExpandedText(expandedText, 0 != (flags & DFlags.ExpandDown)); _SetFooter(footer); Title(title); if (flags.Has(DFlags.Wider)) Wider(700); CanBeMinimized = flags.Has(DFlags.MinimizeButton); if (flags.Has(DFlags.Topmost)) Topmost = true; else if (flags.Has(DFlags.NoTopmost)) Topmost = false; } #region set properties void _SetFlag(_TDF flag, bool on) { if (on) _c.dwFlags |= flag; else _c.dwFlags &= ~flag; } bool _HasFlag(_TDF flag) { return (_c.dwFlags & flag) != 0; } /// /// Sets title bar text. /// If not set, will use . /// public dialog Title(string title) { _c.pszWindowTitle = title.NE() ? options.defaultTitle : title; //info: if "", API uses "ProcessName.exe". return this; } /// /// Sets heading text. /// /// Text. Can be null. public dialog Text1(string text) { _c.pszMainInstruction = text; return this; } /// /// Sets message text. /// /// Text. Can be string, or string with links like link text.", e => { print.it("link"); })]]>, or null. public dialog Text2(DText text) { _c.pszContent = _DTextGetText(text, 0); return this; } string _DTextGetText(DText t, int caller) { if (t is null || t.text.NE()) return null; if (t.links.NE_()) return t.text; (_links ??= [[], [], []])[caller] = t.links; //replace old items in _links int i = 0; return t.text.RxReplace(@".+?)", m => { if (i == t.links.Length) return ""; return $" href=\"{c_guidLink};{caller};{i++}\""; }); } const string c_guidLink = "37de6377cf1c43a2a6eb9a7945fca3c0"; Action[][] _links; bool _DTextLinkClicked(string href) { if (_links != null && href is { Length: > 35 } s && s.Starts(c_guidLink) && s.ToInt(out int caller, 33) && (uint)caller < 3 && s.ToInt(out int i, 35) && (uint)i < _links[caller].Length) { _links[caller][i]?.Invoke(new(this, _dlg, DNative.TDN.HYPERLINK_CLICKED, 0, 0, 0, null)); return true; } return false; } /// /// Sets common icon. /// /// /// The value also can be a native icon group resource id (cast to ), in range 1 to 0xf000. /// public dialog Icon(DIcon icon) { _c.hMainIcon = (nint)icon; _iconGC = null; return this; } /// /// Sets custom icon. /// /// Can be: ///
. ///
. ///
IntPtr - native icon handle. ///
. ///
• string - XAML image, eg copied from the Icons tool. See . /// /// /// The icon should be of logical size 32 or 16. /// public dialog Icon(object icon) { _iconGC = icon; //will set when showing dialog, because may need screen DPI _c.hMainIcon = 0; return this; //tested: displays original-size 32 and 16 icons, but shrinks bigger icons to 32. } object _iconGC; //GC #region buttons const int c_idOK = 1; const int c_idCancel = 2; const int c_idRetry = 4; const int c_idYes = 6; const int c_idNo = 7; const int c_idClose = 8; const int c_idTimeout = int.MinValue; /// /// The return value of ShowX functions on timeout. /// public const int Timeout = int.MinValue; _Buttons _buttons; struct _Buttons { List<(int id, string s)> _customButtons, _radioButtons; int _defaultButtonUserId; bool _isDefaultButtonSet; public int DefaultButtonUserId { get => _defaultButtonUserId; set { _defaultButtonUserId = value; _isDefaultButtonSet = true; } } bool _hasXButton; public _TDCBF SetButtons(Strings buttons, Strings customButtons) { _customButtons = null; _mapIdUserNative = null; _defaultButtonUserId = 0; _isDefaultButtonSet = false; switch (customButtons.Value) { case string s: _ParseButtons(s, true); break; case IEnumerable e: int id = 0; foreach (var v in e) { string s = _ParseSingleString(v, ref id, true); (_customButtons ??= new()).Add((id, s)); } DefaultButtonUserId = 1; break; } return _ParseButtons(buttons, false); } _TDCBF _ParseButtons(Strings buttons, bool onlyCustom) { var ba = buttons.ToArray(); if (ba.NE_()) return 0; _TDCBF commonButtons = 0; int id = 0, nextNativeId = 100; foreach (var v in ba) { string s = _ParseSingleString(v, ref id, onlyCustom); int nativeId = 0; if (!onlyCustom) { switch (s) { case "OK": commonButtons |= _TDCBF.OK; nativeId = c_idOK; break; case "Yes": commonButtons |= _TDCBF.Yes; nativeId = c_idYes; break; case "No": commonButtons |= _TDCBF.No; nativeId = c_idNo; break; case "Cancel": commonButtons |= _TDCBF.Cancel; nativeId = c_idCancel; break; case "Retry": commonButtons |= _TDCBF.Retry; nativeId = c_idRetry; break; case "Close": commonButtons |= _TDCBF.Close; nativeId = c_idClose; break; } } if (nativeId == 0) { //custom button (_customButtons ??= new()).Add((id, s)); if (id < 0) nativeId = nextNativeId++; //need to map, because native ids of positive user ids are minus user ids } if (nativeId != 0) (_mapIdUserNative ??= new()).Add((id, nativeId)); if (!_isDefaultButtonSet) DefaultButtonUserId = id; } return commonButtons; } static string _ParseSingleString(string s, ref int id, bool dontSplit) { if (!dontSplit && StringUtil.ParseIntAndString(s, out var i, out string r)) id = i; else { r = s; id++; } r = r.Trim("\r\n"); //API does not like newline at start, etc if (r.Length == 0) r = " "; //else API exception else r = r.Replace("\r\n", "\n"); //API adds 2 newlines for \r\n. Only for custom buttons, not for other controls/parts. return r; } public void SetRadioButtons(Strings buttons) { _radioButtons = null; var ba = buttons.ToArray(); if (ba.NE_()) return; _radioButtons = new(); int id = 0; foreach (var v in ba) { string s = _ParseSingleString(v, ref id, false); _radioButtons.Add((id, s)); } } List<(int userId, int nativeId)> _mapIdUserNative; public int MapIdUserToNative(int userId) { if (userId == c_idTimeout) return userId; //0x80000000 if (_mapIdUserNative != null) { //common buttons, and custom buttons with negative user id foreach (var v in _mapIdUserNative) if (v.userId == userId) return v.nativeId; } return -userId; //custom button with positive user id } public int MapIdNativeToUser(int nativeId) { if (nativeId == c_idTimeout) return nativeId; //0x80000000 if (nativeId <= 0) return -nativeId; //custom button with positive user id if (_mapIdUserNative != null) { //common buttons, and custom buttons with negative user id foreach (var v in _mapIdUserNative) if (v.nativeId == nativeId) return v.userId; } if (nativeId == c_idOK) return nativeId; //single OK button auto-added when no buttons specified Debug.Assert(nativeId == c_idCancel && _hasXButton); return 0; } /// /// Sets c.pButtons, c.cButtons, c.pRadioButtons and c.cRadioButtons. /// Later call MarshalFreeButtons. /// public unsafe void MarshalButtons(ref TASKDIALOGCONFIG c) { c.pButtons = _MarshalButtons(false, out c.cButtons); c.pRadioButtons = _MarshalButtons(true, out c.cRadioButtons); _hasXButton = ((c.dwFlags & _TDF.ALLOW_DIALOG_CANCELLATION) != 0); } /// /// Frees memory allocated by MarshalButtons and sets the c members to null/0. /// public unsafe void MarshalFreeButtons(ref TASKDIALOGCONFIG c) { MemoryUtil.Free(c.pButtons); MemoryUtil.Free(c.pRadioButtons); c.pButtons = null; c.pRadioButtons = null; c.cButtons = 0; c.cRadioButtons = 0; } unsafe TASKDIALOG_BUTTON* _MarshalButtons(bool radio, out int nButtons) { var a = radio ? _radioButtons : _customButtons; int n = a == null ? 0 : a.Count; nButtons = n; if (n == 0) return null; int nba = n * sizeof(TASKDIALOG_BUTTON), nb = nba; foreach (var v in a) nb += (v.s.Length + 1) * 2; var r = (TASKDIALOG_BUTTON*)MemoryUtil.Alloc(nb); char* s = (char*)((byte*)r + nba); for (int i = 0; i < n; i++) { var v = a[i]; r[i].id = radio ? v.id : MapIdUserToNative(v.id); int len = v.s.Length + 1; r[i].text = Api.lstrcpyn(s, v.s, len); s += len; } return r; } } /// /// Sets buttons. /// /// /// List of button names or "id name". Examples: "OK|Cancel", "1 Yes|2 No", , ["1 One", "2 Two"]. /// Can contain common buttons (named OK, Yes, No, Retry, Cancel, Close) and/or custom buttons (any other names). /// This first in the list button will be focused (aka default button). /// More info in Remarks. /// /// The style of custom buttons. If false - row of classic buttons. If true - column of command-link buttons that can have multiline text. /// /// If buttons not set, the dialog will have OK button, id 1. /// /// Missing ids are auto-generated, for example "OK|Cancel|100 Custom1|Custom2" is the same as "1 OK|2 Cancel|100 Custom1|101 Custom2". /// /// The first in the list button is the default button, ie is focused and therefore responds to the Enter key. For example, "2 No|1 Yes" adds Yes and No buttons and makes No default. /// /// To create keyboard shortcuts, use & character in custom button labels. Use && for literal &. Example: . /// /// Trims newlines around ids and labels. For example, "\r\n1 One\r\n|\r\n2\r\nTwo\r\n\r\n" is the same as "1 One|2 Two". /// /// There are 6 common buttons: OK, Yes, No, Retry, Cancel, Close. Buttons that have other names are custom buttons. /// How common buttons are different: /// 1. The button style is not affected by asCommandLinks or . /// 2. They have keyboard shortcuts that cannot be changed. Inserting & in a label makes it a custom button. /// 3. Button Cancel can be selected with the Esc key. It also adds X (Close) button in title bar, which selects Cancel. /// 4. Always displayed in standard order (eg Yes No, never No Yes). But you can for example use "2 No|1 Yes" to set default button = No. /// 5. The displayed button label is localized, ie different when the Windows UI language is not English. /// public dialog Buttons(Strings buttons = default, bool asCommandLinks = false) { _btn.buttons = buttons; _btn.commandLinks = asCommandLinks; return this; } /// /// Sets custom buttons to be displayed as a list. /// /// /// List of button names. Can be string like "One|Two|..." or string[] or List<string>. /// Button ids will be 1, 2, ... . Default button will be 1, unless changed with . /// Unlike , this function does not allow to specify button ids; also all specified buttons will be custom buttons, even if named like "OK". /// /// The style of custom buttons. If false - row of classic buttons. If true - column of command-link buttons that can have multiline text. /// /// You can call too, to add common buttons (like OK, Cancel); use negative button ids. Both functions set the style (classic or command link) of custom buttons; wins the one called last. /// public dialog ButtonsList(Strings buttons = default, bool asCommandLinks = true) { _btn.list = buttons; _btn.commandLinks = asCommandLinks; return this; } (Strings buttons, Strings list, bool commandLinks, int idDefault) _btn; /// /// Sets default button. It responds to the Enter key. /// /// Button id. If 0 - the first button in the list. public dialog Default(int id) { _btn.idDefault = id; return this; } /// /// Adds radio buttons. /// /// A list of strings "id text" separated by |, like "1 One|2 Two|3 Three". /// Check the radio button that has this id. If omitted or 0, checks the first. If negative, does not check. /// /// To get selected radio button id after closing the dialog, use . /// public dialog RadioButtons(Strings buttons, int idDefault = 0) { _controls ??= new(); _controls.RadioButtons = buttons; _controls.RadioId = idDefault; return this; } #endregion buttons /// /// Adds check box (if text is not null/empty). /// /// /// To get check box state after closing the dialog, use . /// public dialog Checkbox(string text, bool check = false) { _controls ??= new(); _controls.Checkbox = text; _controls.IsChecked = check; return this; } /// /// Adds text in expander control. /// /// Show the text at the bottom of the dialog. /// public dialog ExpandedText(DText text, bool showInFooter = false) { string s = _DTextGetText(text, 1); _SetFlag(_TDF.EXPAND_FOOTER_AREA, showInFooter); _c.pszExpandedInformation = s; return this; } /// /// Set properties of the expander control that shows and hides text added by . /// /// /// /// public dialog Expander(bool expand, string collapsedText = null, string expandedText = null) { _SetFlag(_TDF.EXPANDED_BY_DEFAULT, expand); _c.pszCollapsedControlText = collapsedText; _c.pszExpandedControlText = expandedText; return this; } /// /// Adds footer text. /// /// public dialog FooterText(DText text) { _footerText = _DTextGetText(text, 2); return this; } string _footerText; /// /// Adds footer icon. /// /// /// The value also can be a native icon group resource id (cast to ), in range 1 to 0xf000. /// public dialog FooterIcon(DIcon icon) { _c.hFooterIcon = (nint)icon; _iconFooterGC = null; return this; } /// /// Sets footer icon. /// /// /// /// The icon should be of logical size 16. /// public dialog FooterIcon(object icon) { _iconFooterGC = icon; //will set when showing dialog, because may need screen DPI _c.hFooterIcon = 0; return this; } object _iconFooterGC; //GC //Used by ctor. Supports string with icon, like "i|Info". void _SetFooter(DText text) { var s = _DTextGetText(text, 2); if (s?.Eq(1, '|') ?? false) { FooterIcon(s[0] switch { 'x' => DIcon.Error, '!' => DIcon.Warning, 'i' => DIcon.Info, 'v' => DIcon.Shield, 'a' => DIcon.App, _ => 0 }); s = s[2..]; } _footerText = s; } /// /// Adds Edit or ComboBox control. /// /// Control type/style. /// Initial edit field text. /// Combo box items used when editType is . /// /// To get control text after closing the dialog, use . /// /// Dialogs with an input field cannot have a progress bar. /// public dialog Edit(DEdit editType, string editText = null, Strings comboItems = default) { _controls ??= new(); _controls.EditType = editType; _controls.EditText = editText; _controls.ComboItems = comboItems; return this; } /// /// Makes the dialog wider. /// /// /// Width of the dialog's client area, in logical pixels. The actual width depends on the screen DPI. /// If the value is less than default width, will be used default width. /// /// public dialog Wider(int width) { _c.cxWidth = width / 2; return this; } /// /// Sets owner window. /// /// Owner window, or one of its child/descendant controls. Can be , WPF window or element, winforms window or control. Can be null. /// Show the dialog in the center of the owner window. /// Don't disable the owner window. If false, disables if it belongs to this thread. /// /// The owner window will be disabled, and this dialog will be on top of it. /// This window will be in owner's screen, if screen was not explicitly specified (see ). is ignored. /// /// public dialog OwnerWindow(AnyWnd owner, bool ownerCenter = false, bool dontDisable = false) { _c.hwndParent = owner.IsEmpty ? default : owner.Hwnd.Window; _SetFlag(_TDF.POSITION_RELATIVE_TO_WINDOW, ownerCenter); _enableOwner = dontDisable; return this; } bool _enableOwner; /// /// Sets dialog position in screen. /// /// X position in screen. If default - screen center. Examples: 10, ^10 (reverse), .5f (fraction). /// Y position in screen. If default - screen center. /// x y are relative to the primary screen (ignore etc). /// public dialog XY(Coord x, Coord y, bool rawXY = false) { _x = x; _y = y; _rawXY = rawXY && (!_x.IsEmpty || !_y.IsEmpty); return this; } Coord _x, _y; bool _rawXY; /// /// Sets the screen (display monitor) where to show the dialog in multi-screen environment. /// /// /// If not set, will be used owner window's screen or . /// More info: , . /// public dialog InScreen(screen screen) { Screen = screen; return this; } /// /// Sets to automatically close the dialog after timeoutS seconds. Then returns . /// /// Timeout in seconds. /// Short text to display what will happen on timeout. For example button name. See . /// Don't display the timeout information in the footer. public dialog CloseAfter(int timeoutS, string timeoutAction = null, bool noInfo = false) { _timeoutS = timeoutS; _timeoutActionText = timeoutAction; _timeoutNoInfo = noInfo; return this; } int _timeoutS; bool _timeoutActive, _timeoutNoInfo; string _timeoutActionText; /// /// Sets progress bar. /// /// Whether to show progress bar. /// Show just an animation that does not indicate a progress. /// /// To start or stop the marquee animation, use code like this when the dialog is visible: d.Send.Message(DNative.TDM.SET_PROGRESS_BAR_MARQUEE, 1);. /// To set progress, use when the dialog is visible. Example in . /// public dialog Progress(bool show, bool marquee = false) { ProgressBar = show && !marquee; ProgressBarMarquee = show && marquee; return this; } #endregion set properties #region old hidden non-fluent set properties /// [EditorBrowsable(EditorBrowsableState.Never)] public screen Screen { set; get; } /// /// Show progress bar. /// [EditorBrowsable(EditorBrowsableState.Never)] public bool ProgressBar { set; get; } /// /// Show progress bar that does not indicate which part of the work is already done. /// [EditorBrowsable(EditorBrowsableState.Never)] public bool ProgressBarMarquee { set; get; } /// /// Right-to left layout. /// Default = . /// [EditorBrowsable(EditorBrowsableState.Never)] //use options.rtlLayout instead public bool RtlLayout { set; get; } /// /// Add Minimize button to the title bar. /// [EditorBrowsable(EditorBrowsableState.Never)] //use DFlags.MinimizeButton instead public bool CanBeMinimized { set; get; } /// /// Makes the dialog window topmost or non-topmost. /// /// /// true - set topmost style when creating the dialog. /// false - don't set. /// null (default) - topmost if both these are true: no owner window, is true (default). /// [EditorBrowsable(EditorBrowsableState.Never)] public bool? Topmost { set; get; } #endregion wnd _dlg; int _threadIdInShow; bool _locked; /// /// Shows the dialog. /// Call this method after setting text and other properties. /// /// Selected button id. /// Failed to show dialog. Unlikely. public unsafe int ShowDialog() { //info: named ShowDialog, not Show, to not confuse with the static Show() which is used almost everywhere in documentation. _result = 0; _isClosed = false; Title(_c.pszWindowTitle); //if not set, sets default _EditControlInitBeforeShowDialog(); //don't reorder, must be before flags if (_c.hwndParent.Is0 && options.autoOwnerWindow) { var wa = wnd.thisThread.active; if (wa.Is0) wa = wnd.getwnd.TopThreadWindow_(onlyVisible: true, nonPopup: true); _c.hwndParent = wa; //info: MessageBox.Show also does it, but it also disables all thread windows } if (_c.hwndParent.IsAlive) { if (!_enableOwner && !_c.hwndParent.IsOfThisThread) _enableOwner = true; if (_enableOwner && !_c.hwndParent.IsEnabled(false)) _enableOwner = false; } _SetPos(true); //get screen _SetFlag(_TDF.SIZE_TO_CONTENT, true); //can make max 50% wider _SetFlag(_TDF.ALLOW_DIALOG_CANCELLATION, _flags.Has(DFlags.XCancel)); _SetFlag(_TDF.RTL_LAYOUT, RtlLayout); _SetFlag(_TDF.CAN_BE_MINIMIZED, CanBeMinimized); _SetFlag(_TDF.SHOW_PROGRESS_BAR, ProgressBar); _SetFlag(_TDF.SHOW_MARQUEE_PROGRESS_BAR, ProgressBarMarquee); _SetFlag(_TDF.ENABLE_HYPERLINKS, _links != null || HyperlinkClicked != null); _SetFlag(_TDF.CALLBACK_TIMER, (_timeoutS > 0 || Timer != null)); _c.dwCommonButtons = _buttons.SetButtons(_btn.buttons, _btn.list); _SetFlag(_TDF.USE_COMMAND_LINKS, _btn.commandLinks); _c.nDefaultButton = _buttons.MapIdUserToNative(_btn.idDefault != 0 ? _btn.idDefault : _buttons.DefaultButtonUserId); if (_controls != null) { _c.pszVerificationText = _controls.Checkbox; _SetFlag(_TDF.VERIFICATION_FLAG_CHECKED, _controls.IsChecked); _buttons.SetRadioButtons(_controls.RadioButtons); _c.nDefaultRadioButton = _controls.RadioId; _SetFlag(_TDF.NO_DEFAULT_RADIO_BUTTON, _controls.RadioId < 0); } _timeoutActive = _timeoutS > 0; _c.pszFooter = _timeoutActive && !_timeoutNoInfo ? _TimeoutFooterText(_timeoutS) : _footerText; screen screenForIcons = default; if (_iconGC != null) _c.hMainIcon = _IconHandle(_iconGC, false); else if (_c.hMainIcon == 0 && options.useAppIcon) _c.hMainIcon = (nint)DIcon.App; if (_iconFooterGC != null) _c.hFooterIcon = _IconHandle(_iconFooterGC, true); _SetFlag(_TDF.USE_HICON_MAIN, _c.hMainIcon != 0 && _iconGC != null); _SetFlag(_TDF.USE_HICON_FOOTER, _c.hFooterIcon != 0 && _iconFooterGC != null); IntPtr _IconHandle(object o, bool small) { if (o is string s) { int k = small ? 16 : 32; if (screenForIcons.IsEmpty) screenForIcons = _GetScreenBeforeShow(); k = Dpi.Scale(k, screenForIcons.Dpi); o = ImageUtil.XamlIconToGdipIcon_(s, k); } return o switch { icon a => a.Handle, System.Drawing.Icon a => a.Handle, System.Drawing.Bitmap a => new icon(a.GetHicon()), IntPtr a => a, _ => 0 }; } if (_iconGC == null && (long)_c.hMainIcon is >= 1 and < 0xf000) _c.hInstance = icon.GetAppIconModuleHandle_((int)_c.hMainIcon); else if (_iconFooterGC == null && (long)_c.hFooterIcon is >= 1 and < 0xf000) _c.hInstance = icon.GetAppIconModuleHandle_((int)_c.hFooterIcon); //info: DIcon.App is IDI_APPLICATION (32512). //Although MSDN does not mention that IDI_APPLICATION can be used when hInstance is NULL, it works. Even works for many other undocumented system resource ids, eg 100. //For App icon we could instead use icon handle, but then the small icon for the title bar and taskbar button can be distorted because shrinked from the big icon. Now extracts small icon from resources. _c.pfCallback = _CallbackProc; int rNativeButton = 0, rRadioButton = 0, rIsChecked = 0, hr = 0; WindowsHook hook = null; try { _threadIdInShow = Environment.CurrentManagedThreadId; _buttons.MarshalButtons(ref _c); if (_c.pButtons == null) _SetFlag(_TDF.USE_COMMAND_LINKS | _TDF.USE_COMMAND_LINKS_NO_ICON, false); //avoid exception if (_timeoutActive) { //Need mouse/key messages to stop countdown on click or key. hook = WindowsHook.ThreadGetMessage(_HookProc); } wnd.Internal_.EnableActivate(true); //TaskDialog[Indirect] API bug: If called simultaneously by 2 threads, often fails and returns an unknown error code 0x800403E9. //Known workarounds: // 1. Lock. Unlock on first callback message. Now used. // 2. Retry. Now used only for other unexpected errors, eg out-of-memory. _LockUnlock(true); for (int i = 0; i < 10; i++) { //see the API bug workaround comment hr = _CallTDI(out rNativeButton, out rRadioButton, out rIsChecked); //if(hr != 0) print.it("0x" + hr.ToString("X"), !_dlg.Is0); if (hr == 0 //succeeded || hr == Api.E_INVALIDARG //will never succeed || hr == unchecked((int)0x8007057A) //invalid cursor handle (custom icon disposed) || !_dlg.Is0 //_dlg is set if our callback function was called; then don't retry, because the dialog was possibly shown, and only then error. ) break; Thread.Sleep(30); } if (hr == 0) { _result = _buttons.MapIdNativeToUser(rNativeButton); if (_controls != null) { _controls.IsChecked = rIsChecked != 0; _controls.RadioId = rRadioButton; } WndUtil.WaitForAnActiveWindow(doEvents: true); } } finally { _LockUnlock(false); //Normally the dialog now is destroyed and _dlg now is 0, because _SetClosed called on the destroy message. //But on exception it is not called and the dialog is still alive and visible. //Therefore Windows shows its annoying "stopped working" UI (cannot reproduce it now with Core). //To avoid it, destroy the dialog now. Also to avoid possible memory leaks etc. if (!_dlg.Is0) Api.DestroyWindow(_dlg); _SetClosed(); _threadIdInShow = 0; hook?.Dispose(); _buttons.MarshalFreeButtons(ref _c); } if (hr != 0) throw new Win32Exception(hr); return _result; } int _CallTDI(out int pnButton, out int pnRadioButton, out int pChecked) { //#if DEBUG // //Debug_.PrintIf("1" != Environment.GetEnvironmentVariable("COMPlus_legacyCorruptedStateExceptionsPolicy"), "no env var COMPlus_legacyCorruptedStateExceptionsPolicy=1"); // pnButton = pnRadioButton = pChecked = 0; // try { //#endif return TaskDialogIndirect(in _c, out pnButton, out pnRadioButton, out pChecked); //#if DEBUG // } // catch (Exception e) { // throw new Win32Exception("_CallTDI: " + e.ToStringWithoutStack()); //note: not just throw;, and don't add inner exception // } // //The API throws 'access violation' exception if some value is invalid (eg unknown flags in dwCommonButtons) or it does not like something. // //By default .NET does not allow to handle eg access violation exceptions. // // Previously we would add [HandleProcessCorruptedStateExceptions], but Core ignores it. // // Now our AppHost sets environment variable COMPlus_legacyCorruptedStateExceptionsPolicy=1 before loading runtime. // // Or could move the API call to the C++ dll. //#endif } void _LockUnlock(bool on) { var obj = "/0p4oSiwoE+7Saqf30udQQ"; if (on) { Debug.Assert(!_locked); _locked = false; Monitor.Enter(obj, ref _locked); } else if (_locked) { Monitor.Exit(obj); _locked = false; } } //Called twice: // 1. Before showing dialog, to get screen while the dialog still isn't the active window if need. On TDN.CREATED screen.ofActiveWindow would be bad. // 2. On TDN.CREATED, to move dialog if need. void _SetPos(bool before) { if (before) _screenForMove = default; if (_HasFlag(_TDF.POSITION_RELATIVE_TO_WINDOW)) return; if (_flags.Has(DFlags.CenterMouse)) { if (!before) { var p = mouse.xy; var scrn = screen.of(p); if (screen.of(_dlg).Handle != scrn.Handle) _dlg.MoveL_(scrn.Rect.XY); //resize if different DPI var r = _dlg.Rect; r.Move(p.x - r.Width / 2, p.y - 20); r.EnsureInScreen(scrn); _dlg.MoveL(r.left, r.top); } } else if (!_rawXY) { if (before) { _screenForMove = Screen; if (_screenForMove.IsEmpty && _c.hwndParent.Is0) _screenForMove = options.defaultScreen; if (_screenForMove.LazyFunc != null) _screenForMove = _screenForMove.Now; } else if (!_x.IsEmpty || !_y.IsEmpty || !_screenForMove.IsEmpty) { _dlg.MoveInScreen(_x, _y, _screenForMove); } } else if (!before) { _dlg.Move(_x, _y); _dlg.EnsureInScreen(); } } screen _screenForMove; //To get DPI for icons. screen _GetScreenBeforeShow() { if (!_screenForMove.IsEmpty) return _screenForMove; if (Api.GetSystemMetrics(Api.SM_CMONITORS) > 1) { if (_HasFlag(_TDF.POSITION_RELATIVE_TO_WINDOW)) return screen.of(_c.hwndParent); //ownerCenter if (_flags.Has(DFlags.CenterMouse)) return screen.of(mouse.xy); if (_rawXY) return screen.of(Coord.Normalize(_x, _y, centerIfEmpty: true)); if (!_c.hwndParent.Is0) return screen.of(_c.hwndParent); } return screen.primary; } int _CallbackProc(wnd w, DNative.TDN message, nint wParam, nint lParam, IntPtr data) { Action e = null; int R = 0, button = 0, timerTime = 0; string linkHref = null; //print.it(message); switch (message) { case DNative.TDN.DIALOG_CONSTRUCTED: _LockUnlock(false); Send = new DSend(this); //note: must be before setting _dlg, because another thread may call if(d.IsOpen) d.Send.Message(..). _dlg = w; break; case DNative.TDN.DESTROYED: //print.it(w.IsAlive); //valid e = Destroyed; break; case DNative.TDN.CREATED: if (_enableOwner) _c.hwndParent.Enable(true); _SetPos(false); if (Topmost ?? (_c.hwndParent.Is0 && options.topmostIfNoOwnerWindow)) w.ZorderTopmost(); //w.SetStyleAdd(WS.THICKFRAME); //does not work if (_IsEdit) _EditControlCreate(); else if (ProgressBarMarquee) Send.Message(DNative.TDM.SET_PROGRESS_BAR_MARQUEE, 1); //if(FlagKeyboardShortcutsVisible) w.Post(Api.WM_UPDATEUISTATE, 0x30002); //rejected. Don't need too many rarely used features. //fix API bug: dialog window is hidden if process STARTUPINFO specifies hidden window timer.after(1, _ => _dlg.ShowL(true)); //use timer because at this time still invisible always e = Created; break; case DNative.TDN.TIMER: timerTime = (int)wParam; if (_timeoutActive) { int timeElapsed = timerTime / 1000; if (timeElapsed < _timeoutS) { if (!_timeoutNoInfo) Send.ChangeFooterText(_TimeoutFooterText(_timeoutS - timeElapsed - 1), false); } else { _timeoutActive = false; Send.Close(c_idTimeout); } } e = Timer; break; case DNative.TDN.BUTTON_CLICKED: e = ButtonClicked; button = _buttons.MapIdNativeToUser((int)wParam); break; case DNative.TDN.HYPERLINK_CLICKED: linkHref = Marshal.PtrToStringUni(lParam); if (_DTextLinkClicked(linkHref)) return 0; e = HyperlinkClicked; break; case DNative.TDN.HELP: e = HelpF1; break; default: e = OtherEvents; break; } if (_IsEdit) _EditControlOnMessage(message); if (e != null) { var ed = new DEventArgs(this, _dlg, message, wParam, button, timerTime, linkHref); e(ed); R = ed.returnValue; } if (message == DNative.TDN.DESTROYED) _SetClosed(); return R; } /// /// After the dialog has been created and before it is displayed. /// public event Action Created; /// /// When the dialog is closed and its window handle is no longer valid. /// public event Action Destroyed; /// /// Every 200 ms. /// /// /// { print.it(e.TimerTimeMS); }; /// d.ShowDialog(); /// ]]> /// public event Action Timer; /// /// When the user selects a button. /// /// /// { print.it(e.Button); e.DontCloseDialog = e.Button == 2; }; /// d.ShowDialog(); /// ]]> /// public event Action ButtonClicked; /// /// When the user clicks a hyperlink in the dialog text. /// /// /// links."); /// d.HyperlinkClicked += e => { print.it(e.LinkHref); }; /// d.ShowDialog(); /// ]]> /// public event Action HyperlinkClicked; /// /// When the user presses F1. /// /// /// { run.it("https://www.google.com/search?q=more+info"); }; /// d.ShowDialog(); /// ]]> /// public event Action HelpF1; /// /// Events other than , , , , , . See API TaskDialogCallbackProc. /// public event Action OtherEvents; #region async etc /// /// Shows the dialog in new thread and returns without waiting until it is closed. /// /// /// Calls , therefore the dialog is already open when this function returns. /// More info: /// /// Failed to show dialog. Unlikely. public void ShowDialogNoWait() { Exception ex = null; Task.Run(() => { try { ShowDialog(); } catch (Exception e) { ex = e; } }); if (!ThreadWaitForOpen()) throw ex ?? new Win32Exception(0); } /// /// Selected button id. The same as the return value. /// /// /// If the result is still unavailable (the dialog still not closed): /// - If called from the same thread that called , returns 0. /// - If called from another thread, waits until the dialog is closed. /// /// Note: calls in another thread. /// public int Result { get { if (!_WaitWhileInShow()) return 0; return _result; } } int _result; /// /// After closing the dialog contains values of checkbox, radio buttons and/or text edit control. /// null if no controls. /// public DControls Controls => _controls; DControls _controls; bool _WaitWhileInShow() { if (_threadIdInShow != 0) { if (_threadIdInShow == Environment.CurrentManagedThreadId) return false; while (_threadIdInShow != 0) Thread.Sleep(15); } return true; } /// /// Can be used by other threads to wait until the dialog is open. /// /// ///
true - the dialog is open and you can send messages to it. ///
false - the dialog is already closed or failed to show. ///
public bool ThreadWaitForOpen() { _AssertIsOtherThread(); while (!IsOpen) { if (_isClosed) return false; wait.doEvents(15); //need 2-3 loops if 15. Without doEvents hangs if a form is the dialog owner. } return true; } /// /// Can be used by other threads to wait until the dialog is closed. /// public void ThreadWaitForClosed() { _AssertIsOtherThread(); while (!_isClosed) { Thread.Sleep(30); } _WaitWhileInShow(); } void _AssertIsOtherThread() { if (_threadIdInShow != 0 && _threadIdInShow == Environment.CurrentManagedThreadId) throw new AuException("wrong thread"); } /// /// Returns true if the dialog is open and your code can send messages to it. /// public bool IsOpen => !_dlg.Is0; void _SetClosed() { _isClosed = true; if (_dlg.Is0) return; _dlg = default; Send.Clear_(); } bool _isClosed; #endregion async etc #region send messages /// /// Gets dialog window handle as . /// /// default(wnd) if the dialog is not open. public wnd DialogWindow => _dlg; /// /// Allows to modify dialog controls while it is open, and close the dialog. /// /// /// Example: d.Send.Close(); . /// Example: d.Send.ChangeText2("new text", false); . /// Example: d.Send.Message(DNative.TDM.CLICK_VERIFICATION, 1); . /// /// Can be used only while the dialog is open. Before showing the dialog returns null. After closing the dialog the returned variable is deactivated; its method calls are ignored. /// Can be used in dialog event handlers. Also can be used in another thread, for example with and . /// public DSend Send { get; private set; } //called by DSend internal int SendMessage_(DNative.TDM message, nint wParam = 0, nint lParam = 0) { switch (message) { case DNative.TDM.CLICK_BUTTON: case DNative.TDM.ENABLE_BUTTON: case DNative.TDM.SET_BUTTON_ELEVATION_REQUIRED_STATE: wParam = _buttons.MapIdUserToNative((int)wParam); break; } return (int)_dlg.Send((int)message, wParam, lParam); } //called by DSend internal void SetText_(bool resizeDialog, DNative.TDE partId, string text) { if (partId == DNative.TDE.CONTENT && (_controls?.EditType ?? default) == DEdit.Multiline) { text = _c.pszContent = text + c_multilineString; } _dlg.Send((int)(resizeDialog ? DNative.TDM.SET_ELEMENT_TEXT : DNative.TDM.UPDATE_ELEMENT_TEXT), (int)partId, text ?? ""); //info: null does not change text. if (_IsEdit) _EditControlUpdateAsync(!resizeDialog); //info: sometimes even UPDATE_ELEMENT_TEXT sends our control to the bottom of the Z order. } #endregion send messages #region hookProc, timeoutText //Disables timeout on click or key. unsafe void _HookProc(HookData.ThreadGetMessage d) { switch (d.msg->message) { case Api.WM_LBUTTONDOWN: case Api.WM_NCLBUTTONDOWN: case Api.WM_RBUTTONDOWN: case Api.WM_NCRBUTTONDOWN: case Api.WM_KEYDOWN: case Api.WM_SYSKEYDOWN: if (_timeoutActive && d.msg->hwnd.Window == _dlg) { _timeoutActive = false; Send.ChangeFooterText(_footerText, false); } break; } } string _TimeoutFooterText(int timeLeft) { var format = options.timeoutTextFormat; if (format.NE()) return _footerText; if (_timeoutActionText.NE()) format = format.Lines()[0]; using (new StringBuilder_(out var b)) { b.AppendFormat(format, timeLeft, _timeoutActionText); if (!_footerText.NE()) b.Append('\n').Append(_footerText); return b.ToString(); } } #endregion hookProc, timeoutText #region Edit control //never mind: our edit control disappears when moving the dialog to a screen with different DPI bool _IsEdit => _controls != null && _controls.EditType != DEdit.None; void _EditControlInitBeforeShowDialog() { if (!_IsEdit) return; ProgressBarMarquee = true; ProgressBar = false; _c.pszContent ??= ""; if (_c.pszExpandedInformation != null && _controls.EditType == DEdit.Multiline) _SetFlag(_TDF.EXPAND_FOOTER_AREA, true); } void _EditControlUpdate(bool onlyZorder = false) { if (_editWnd.Is0) return; if (!onlyZorder) { _EditControlGetPlace(out RECT r); _editParent.MoveL(r); _editWnd.MoveL(0, 0, r.Width, r.Height); } _editParent.ZorderTopRaw_(); } void _EditControlUpdateAsync(bool onlyZorder = false) { _editParent.Post(Api.WM_APP + 111, onlyZorder ? 1 : 0); } //to reserve space for multiline Edit control we append this to text2 const string c_multilineString = "\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n "; wnd _EditControlGetPlace(out RECT r) { wnd parent = _dlg; //don't use the DirectUIHWND control for it, it can create problems //create or get cached font and calculate control height _editFont = NativeFont_.RegularCached(Dpi.OfWindow(parent)); //tested: on Win8.1 API isn't PM-DPI-aware. //We'll hide the progress bar control and create our Edit control in its place. wnd prog = parent.Child(cn: "msctls_progress32", flags: WCFlags.HiddenToo); prog.GetRectIn(parent, out r); if (_controls.EditType == DEdit.Multiline) { int top = r.top; if (!_c.pszContent.Ends(c_multilineString)) { _c.pszContent += c_multilineString; _dlg.Send((int)DNative.TDM.SET_ELEMENT_TEXT, (int)DNative.TDE.CONTENT, _c.pszContent); prog.GetRectIn(parent, out r); //used to calculate Edit control height: after changing text, prog is moved down, and we know its previous location... } if (_editMultilineHeight == 0) { _editMultilineHeight = r.bottom - top; } else top = r.bottom - _editMultilineHeight; r.top = top; } else { r.top = r.bottom - (_editFont.HeightOnScreen + 8); } prog.ShowL(false); return parent; } int _editMultilineHeight; void _EditControlCreate() { wnd parent = _EditControlGetPlace(out RECT r); //Create an intermediate "#32770" to be direct parent of the Edit control. //It is safer (the dialog will not receive Edit notifications) and helps to solve Tab/Esc problems. var pStyle = WS.CHILD | WS.VISIBLE | WS.CLIPCHILDREN | WS.CLIPSIBLINGS; //don't need WS_TABSTOP var pExStyle = WSE.NOPARENTNOTIFY; //not WSE.CONTROLPARENT _editParent = WndUtil.CreateWindow("#32770", null, pStyle, pExStyle, r.left, r.top, r.Width, r.Height, parent); Api.SetWindowLongPtr(_editParent, GWL.DWL.DLGPROC, Marshal.GetFunctionPointerForDelegate(_editControlParentProcHolder = _EditControlParentProc)); //Create Edit or ComboBox control. string cn = "Edit"; var style = WS.CHILD | WS.VISIBLE; //don't need WS_TABSTOP switch (_controls.EditType) { case DEdit.Text: style |= Api.ES_AUTOHSCROLL; break; case DEdit.Password: style |= Api.ES_PASSWORD | Api.ES_AUTOHSCROLL; break; case DEdit.Number: style |= Api.ES_NUMBER | Api.ES_AUTOHSCROLL; break; case DEdit.Multiline: style |= Api.ES_MULTILINE | Api.ES_AUTOVSCROLL | Api.ES_WANTRETURN | WS.VSCROLL; break; case DEdit.Combo: style |= Api.CBS_DROPDOWN | Api.CBS_AUTOHSCROLL | WS.VSCROLL; cn = "ComboBox"; break; } _editWnd = WndUtil.CreateWindow(cn, null, style, WSE.CLIENTEDGE, 0, 0, r.Width, r.Height, _editParent); WndUtil.SetFont(_editWnd, _editFont); //Init the control. _editWnd.SetText(_controls.EditText); if (_controls.EditType == DEdit.Combo) { if (_controls.ComboItems.Value != null) { foreach (var s in _controls.ComboItems.ToArray()) _editWnd.Send(Api.CB_INSERTSTRING, -1, s); } RECT cbr = _editWnd.Rect; _editParent.ResizeL(cbr.Width, cbr.Height); //because ComboBox resizes itself } else { _editWnd.Send(Api.EM_SETSEL, 0, -1); } _editParent.ZorderTopRaw_(); Api.SetFocus(_editWnd); } void _EditControlOnMessage(DNative.TDN message) { switch (message) { case DNative.TDN.BUTTON_CLICKED: _controls.EditText = _editWnd.ControlText; break; case DNative.TDN.EXPANDO_BUTTON_CLICKED: case DNative.TDN.NAVIGATED: _EditControlUpdateAsync(); //when expando clicked, sync does not work even with doevents break; } } /// /// Gets edit control handle as . /// public wnd EditControl => _editWnd; wnd _editWnd, _editParent; NativeFont_ _editFont; //Dlgproc of our intermediate #32770 control, the parent of out Edit control. nint _EditControlParentProc(wnd w, int msg, nint wParam, nint lParam) { switch (msg) { case Api.WM_SETFOCUS: //enables Tab when in single-line Edit control Api.SetFocus(_dlg.ChildFast(null, "DirectUIHWND")); return 1; case Api.WM_NEXTDLGCTL: //enables Tab when in multi-line Edit control Api.SetFocus(_dlg.ChildFast(null, "DirectUIHWND")); return 1; case Api.WM_CLOSE: //enables Esc when in edit control _dlg.Send(msg); return 1; case Api.WM_APP + 111: //async update edit control pos _EditControlUpdate(wParam != 0); return 1; } return 0; //BAD: Alt+key doesn't work when the edit control is focused. // https://github.com/qgindi/LibreAutomate/issues/28 // Also we receive 500 pairs of WM_GETDLGCODE + WM_GETTEXT. // To fix this, probably would need a getmsg hook. It could detect the event and focus a button. Too expensive. } WNDPROC _editControlParentProcHolder; #endregion Edit control } ================================================ FILE: Au/GUI/osd.cs ================================================ using System.Drawing; namespace Au.Types { /// /// Transparent window that can be used for on-screen display. Derived classes on it can draw non-transparent text, rectangle, image, anything. /// public abstract class OsdWindow : IDisposable { wnd _w; /// Destroys the OSD window. protected virtual void Dispose(bool disposing) { if (_w.Is0) return; Api.ShowWindow(_w, 0); _TopmostWorkaroundUndo(); var w = _w; _w = default; if (!Api.DestroyWindow(w)) w.Post(Api.WM_CLOSE); } /// Destroys the OSD window. public void Dispose() => Dispose(true); /// Destroys the OSD window. public void Close() => Dispose(true); /// OSD window handle or default(wnd). public wnd Hwnd => _w; /// /// Returns true if the OSD window is created. /// protected bool IsHandleCreated => !_w.Is0; /// /// Redraws the OSD window immediately. /// Does nothing it is not created or not visible. /// protected void Redraw() { if (!Visible) return; Api.InvalidateRect(_w); Api.UpdateWindow(_w); } /// /// Sets to redraw the OSD window later. /// Does nothing it is not created or not visible. /// protected void Invalidate() { if (!Visible) return; Api.InvalidateRect(_w); } /// /// Gets or sets the opacity of the OSD window, from 0 to 1. /// If 1, completely opaque. If 0, pixels of are transparent, others opaque. If between 0 and 1, partially transparent. /// /// Default is 1. Default in is 0. /// /// This property can be changed after creating OSD window. /// public double Opacity { get => _opacity; set { var v = Math.Min(Math.Max(value, 0.0), 1.0); if (v == _opacity) return; bool was0 = _opacity == 0; _opacity = v; if (!IsHandleCreated) return; _SetOpacity(); if ((v == 0) != was0) Redraw(); } } double _opacity = 1d; void _SetOpacity() { if (_opacity > 0) Api.SetLayeredWindowAttributes(_w, 0, (byte)(uint)(_opacity * 255), 2); else Api.SetLayeredWindowAttributes(_w, (uint)TransparentColor.ToBGR(), 0, 1); //never mind: when resizing an alpha-transparent window by moving the top-left corner, the opposite corner shakes. // It's a Windows problem, and I could not find a workaround. } /// /// Gets or sets transparent color, default 0xF5F4F5. Pixels of this color will be transparent, unless is not 0. /// /// /// This property cannot be changed after creating OSD window. /// Note: when used for transparent text, text edges are blended with this color, and it can become visible if the color is not chosen carefully. /// public ColorInt TransparentColor { get; set; } = 0xF5F4F5; /// /// Gets or sets OSD window size and position in screen. /// /// /// This property can be changed after creating OSD window. /// public virtual RECT Rect { get => _r; set { if (value == _r) return; _r = value; if (IsHandleCreated) { _w.SetWindowPos(SWPFlags.NOACTIVATE, _r.left, _r.top, _r.Width, _r.Height, SpecHWND.TOPMOST); _TopmostWorkaroundApply(); } } } RECT _r; /// /// Gets or sets whether the OSD window is visible. /// The set function calls (it creates OSD window if need) or (it does not destroy the OSD window). /// public bool Visible { get => _w.IsVisible; set { if (value) Show(); else Hide(); } //note: if overridden, calls the override func } /// /// Shows the OSD window. Creates if need. /// /// /// In any case, also moves the window to the top of the Z order. /// public virtual void Show() { if (Visible) { _w.ZorderTopmost(); _TopmostWorkaroundApply(); } else { if (_w.Is0) _CreateWindow(); _w.ShowL(true); _w.ZorderTopmost(); _TopmostWorkaroundApply(); Api.UpdateWindow(_w); } } /// /// Hides the OSD window. Does not destroy; use or for it. /// Does nothing if not created or not visible. /// public virtual void Hide() { if (!Visible) return; _w.ShowL(false); } void _CreateWindow() { //register window class if need. Need another class if shadow. string cn; byte regMask; if (Shadow) { cn = "Au.OSD2"; regMask = 2; } else { cn = "Au.OSD"; regMask = 1; } if ((s_isWinClassRegistered & regMask) == 0) { var ce = new RWCEtc() { style = Api.CS_HREDRAW | Api.CS_VREDRAW, mCursor = MCursor.Arrow }; if (Shadow) ce.style |= Api.CS_DROPSHADOW; WndUtil.RegisterWindowClass(cn, null, ce); s_isWinClassRegistered |= regMask; } var es = WSE.TOOLWINDOW | WSE.TOPMOST | WSE.LAYERED | WSE.TRANSPARENT | WSE.NOACTIVATE; if (ClickToClose) es &= ~WSE.TRANSPARENT; _w = WndUtil.CreateWindow(WndProc, true, cn, Name, WS.POPUP, es); //note: don't set rect here: can be painting problems when resizing _SetOpacity(); if (!_r.Is0) _w.SetWindowPos(SWPFlags.NOACTIVATE, _r.left, _r.top, _r.Width, _r.Height, SpecHWND.TOPMOST); } static byte s_isWinClassRegistered; /// /// Called when the OSD window receives a message. /// If your derived class overrides this function, it must call base.WndProc and return its return value, except when don't need default processing. /// protected virtual nint WndProc(wnd w, int message, nint wParam, nint lParam) { switch (message) { case Api.WM_NCDESTROY: Api.PostMessage(default, 0, 0, 0); //stop waiting for a message. Never mind: not always need it. Closed?.Invoke(this, EventArgs.Empty); _w = default; break; case Api.WM_ERASEBKGND: return 1; case Api.WM_PAINT: using (var bp = new BufferedPaint(w, true)) { var dc = bp.DC; using var g = Graphics.FromHdc(dc); if (_opacity == 0) g.Clear((Color)TransparentColor); OnPaint(dc, g, bp.Rect); } return default; case Api.WM_MOUSEACTIVATE: return Api.MA_NOACTIVATE; case Api.WM_LBUTTONUP or Api.WM_RBUTTONUP or Api.WM_MBUTTONUP: Clicked?.Invoke(this, message switch { Api.WM_LBUTTONUP => MButton.Left, Api.WM_RBUTTONUP => MButton.Right, _ => MButton.Middle }); if (ClickToClose) w.Post(Api.WM_CLOSE); break; } return Api.DefWindowProc(w, message, wParam, lParam); } /// public event EventHandler Closed; /// public event EventHandler Clicked; /// /// Called when the OSD window must be drawn or redrawn. /// Derived classes should override this function and draw anything. Don't need to call base.OnPaint of , it does nothing. /// /// /// If is 0 (default), g is filled with . Pixels of this color will be transparent. The base class draws only non-transparent areas. /// protected virtual void OnPaint(IntPtr dc, Graphics g, RECT r) { } /// /// If true, the OSD window will have shadow. /// /// /// This property cannot be changed after creating OSD window. /// protected bool Shadow { get; set; } /// /// If true, the OSD window receive mouse messages. Only completely transparent areas don't. The user can click to close the OSD (left, right or middle button). /// /// /// This property cannot be changed after creating OSD window. /// protected bool ClickToClose { get; set; } /// /// OSD window name. Optional, default null. /// /// /// This text is invisible. Can be used to find OSD window. The class name is "Au.OSD"; if with shadow - "Au.OSD2". /// This property cannot be changed after creating OSD window. /// public string Name { get; set; } /// /// Closes all OSD windows of this process. /// /// If not null, closes only OSD windows whose matches this [wildcard expression](xref:wildcard_expression). public static void closeAll([ParamString(PSFormat.Wildex)] string name = null) { foreach (var w in wnd.findAll(name, "**m Au.OSD||Au.OSD2", WOwner.Process(Api.GetCurrentProcessId()))) w.Post(Api.WM_CLOSE); } #region topmost workaround //Workaround for: our topmost window is below some other topmost windows (Win8+). // Window examples: // A. Windows Start menu, search, TaskListThumbnailWnd. // B. uiAccess apps, eg on-screen keyboard, Inspect, NVDA, QM2 uiAccess. // C. Probably Win8 Store apps. // Known workarounds: // 1. Temporarily make the window non-topmost. But it fails for A and probably C. Fails always if this process is not admin. // 2. Temporarily set partially transparent. But it is useful only for on-screen rect. Not useful for menus and toolbars. Fails if not admin. Fails with WPF windows. // 3. Set window region. Too crazy. Tested, does not work with eg OSK, although works with eg Notepad. // 4. Run tools in uiAccess processes. Too crazy and limited. internal bool TopmostWorkaround_ { get; set; } void _TopmostWorkaroundApply() { if (!TopmostWorkaround_ || !osVersion.minWin10) return; //never mind: on Win8 not tested. Would be bad to make full-screen windows transparent. var w = wnd.fromXY(new(_r.CenterX, _r.CenterY), WXYFlags.NeedWindow); bool apply = false; lock (s_twList) { List aosd = null; foreach (var v in s_twList) if (v.w == w) { if (v.a.Contains(this)) return; aosd = v.a; break; } if (w.HasExStyle(WSE.TOPMOST)) { RECT r = w.Rect; r.Inflate(10, 10); if (r.Width > _r.Width && r.Height > _r.Height) { if (!_w.ZorderIsAbove(w)) { if (apply = aosd == null) s_twList.Add((w, new() { this })); else aosd.Add(this); //print.it(apply, w); } } } if (apply && s_twTimer == null) { s_twTimer = new(_ => { List aw = null; lock (s_twList) { foreach (var (w, _) in s_twList) { //print.it(w.GetTransparency(out var op, out var col), op, col); if (!w.GetTransparency(out var op, out var col) || op == null || op.Value > 200) { if (w.IsAlive) (aw ??= new()).Add(w); } } } if (aw != null) foreach (var w in aw) w.SetTransparency(true, 200, noException: true); }); s_twTimer.Every(1000); } } if (apply) w.SetTransparency(true, 200, noException: true); } void _TopmostWorkaroundUndo() { if (!TopmostWorkaround_ || !osVersion.minWin10) return; List aw = null; lock (s_twList) { for (int i = s_twList.Count; --i >= 0;) { var (w, a) = s_twList[i]; if (a.Remove(this) && a.Count == 0) { s_twList.RemoveAt(i); if (w.IsAlive) (aw ??= new()).Add(w); } } if (s_twList.Count == 0 && s_twTimer != null) { s_twTimer.Stop(); s_twTimer = null; } } if (aw != null) foreach (var w in aw) w.SetTransparency(!true, 255, noException: true); } static List<(wnd w, List a)> s_twList = new(); static timer2 s_twTimer; #endregion } /// /// Used by . /// /// Label placement relative to rectangle: 'L' (left), 'R' (right), 'T' (top), 'B' (bottom) or 'I' (inside). Default: 'L'. public record class ORLabelOptions(char Placement, ColorInt? BackgroundColor = null, ColorInt? TextColor = null) { /// public static implicit operator ORLabelOptions(char placement) => new(placement); } } namespace Au { /// /// Shows a mouse-transparent rectangle on screen. Just visible outline, or filled but partially transparent. /// /// /// Creates a temporary partially transparent window, and draws rectangle in it. Can draw multiple rectangles. /// /// /// /// public class osdRect : OsdWindow { /// public osdRect() { Opacity = 0d; } /// /// Gets or sets rectangle color. /// /// /// This property can be changed after creating OSD window. /// /// /// /// public ColorInt Color { get => _color; set { if (value != _color) { _color = value; Redraw(); } } } ColorInt _color = 0; //=0 adds alpha 0xFF /// /// Gets or sets rectangle frame width. /// Used only if is 0 (default). /// /// /// This property can be changed after creating OSD window. /// public int Thickness { get => _thickness; set { if (value != _thickness) { _thickness = value; Redraw(); } } } int _thickness = 3; /// /// Called when the OSD window must be drawn or redrawn. Draws rectangle. More info: . /// protected override void OnPaint(IntPtr dc, Graphics g, RECT ra) { if (_rects == null) { if (Opacity > 0) { g.Clear((Color)_color); } else { g.DrawRectangleInset((Color)_color, _thickness, ra); } } else if (_rects.Length >= 0) { if (Opacity > 0) g.Clear((Color)TransparentColor); using var tr = _hasLabels ? new GdiTextRenderer(dc, Dpi.OfWindow(Hwnd)) : null; for (int i = 0; i < _rects.Length; i++) { var r = _rects[i].r; r.Offset(-Rect.left, -Rect.top); g.DrawRectangleInset((Color)Color, Thickness, r); if (_hasLabels) { var s = _rects[i].s; var z = tr.MeasureText(s); int pad = z.height / 2; z.width += pad; int x = _labelOptions.Placement switch { 'L' => r.left - z.width - 1, 'R' => r.right, 'I' => r.left + Thickness + 1, _ => r.left }; int y = _labelOptions.Placement switch { 'T' => r.top - z.height - 1, 'B' => r.bottom, 'I' => r.top + Thickness + 1, _ => r.top }; if (x < 0) { x = 0; if (z.width >= r.left) z.width = r.left - 1; } RECT rr = new(x, y, z.width, z.height); if (_labelOptions.BackgroundColor != null) g.FillRectangle((Color)_labelOptions.BackgroundColor.Value, rr); else g.FillRectangle(Brushes.PaleGoldenrod, rr); g.DrawRectangle(Pens.DarkGray, rr); rr.Inflate(-pad / 2, 0); tr.DrawText(s, rr, _labelOptions.TextColor?.ToBGR() ?? 0x404040); } } } } (RECT r, string s)[] _rects; bool _hasLabels; ORLabelOptions _labelOptions; /// /// Sets to draw multiple rectangles. /// /// Rectangles. /// Draw labels. The label text is the rectangle index in rects. /// Label options. If char - label placement relative to rectangle: 'L' (left), 'R' (right), 'T' (top), 'B' (bottom) or 'I' (inside). Default: 'L'. /// /// If this function called, will draw multiple rectangles instead of single (unless rects is null). Opacity should be 0 (default). /// public void SetRects(IEnumerable rects, bool indexLabels = false, ORLabelOptions labelOptions = null) { SetRects(indexLabels ? rects?.Select((r, i) => (r, i.ToS())) : rects?.Select(r => (r, (string)null)), labelOptions); } /// /// Sets to draw multiple rectangles with labels. /// /// Rectangles with label text. The text should be short, single line, else may draw clipped. /// public void SetRects(IEnumerable<(RECT r, string s)> rects, ORLabelOptions labelOptions = null) { if (rects == null) { _rects = null; Rect = default; } else { _rects = rects.ToArray(); var r = RECT.FromLTRB(_rects.Min(o => o.r.left), _rects.Min(o => o.r.top), _rects.Max(o => o.r.right), _rects.Max(o => o.r.bottom)); if (_hasLabels = _rects.Any(o => o.s != null)) { int ii = screen.of(r).Dpi; r.Inflate(ii * 2, ii / 4); } Rect = r; _labelOptions = labelOptions ?? new('L'); Redraw(); } } } /// /// Shows text on screen, like a tooltip or with transparent background. /// /// /// Creates a temporary partially transparent window, and draws text in it. /// Most properties cannot be changed after creating OSD window. /// public class osdText : OsdWindow { NativeFont_ _font; /// protected override void Dispose(bool disposing) { base.Dispose(disposing); _font?.Dispose(); _font = null; } /// /// Coordinates. /// Default: null (screen center). /// /// /// Not used if is set. /// This property can be changed after creating OSD window. /// /// /// /// public PopupXY XY { get => _xy; set { _xy = value; if (value != null) _rectIsSet = false; if (IsHandleCreated) base.Rect = Measure(); } } PopupXY _xy; /// /// Gets or sets OSD window size and position in screen. /// /// /// Normally don't need to use this property. If not used, the OSD window size depends on text etc, and position on . /// This property can be changed after creating OSD window. /// /// public override RECT Rect { get => base.Rect; set { _rectIsSet = true; base.Rect = value; } } bool _rectIsSet; void _ResizeOrInvalidate() { if (ResizeWhenContentChanged) base.Rect = Measure(); //info: invalidates if resized, because of the CS_ styles Invalidate(); } /// /// When changing text, resize/move the OSD window if need. /// Default: false. /// public bool ResizeWhenContentChanged { get; set; } /// /// Text in OSD window. /// /// /// This property can be changed after creating OSD window; then the window is not moved/resized, unless is true. /// public string Text { get => _text; set { if (value != _text) { _text = value; _ResizeOrInvalidate(); } } } string _text; /// /// Font. /// Default: . /// /// /// This property cannot be changed after creating OSD window. /// public FontNSS Font { get; set; } /// /// Text color. /// Default: . /// /// /// This property can be changed after creating OSD window. /// public ColorInt TextColor { get => _textColor; set { if (value != _textColor) { _textColor = value; Invalidate(); } } } ColorInt _textColor; /// /// Background color. /// Default: . /// /// /// This property can be changed after creating OSD window. /// Not used for completely transparent OSD. /// public ColorInt BackColor { get => _backColor; set { if (value != _backColor) { _backColor = value; Invalidate(); } } } ColorInt _backColor; /// /// Border color. /// Default: . /// /// /// This property can be changed after creating OSD window. /// No border if ==0 or BorderColor==. /// public ColorInt BorderColor { get => _borderColor; set { if (value != _borderColor) { _borderColor = value; Invalidate(); } } } private ColorInt _borderColor; /// /// Background image. /// /// /// This property cannot be changed after creating OSD window. /// public Image BackgroundImage { get; set; } //public ImageLayout BackgroundImageLayout { get; set; } //FUTURE /// /// When used , the OSD window has the same size as the image, plus borders. /// Else OSD window size is calculated from sizes of text and icon. Then image is displayed scaled or clipped if need. /// /// /// This property cannot be changed after creating OSD window. /// public bool IsOfImageSize { get; set; } /// /// Maximal text width. /// Default: 0 - no limit (depends on screen width etc). /// /// /// This property cannot be changed after creating OSD window. /// public int WrapWidth { get; set; } /// /// Gets or sets text format flags. /// Default: TFFlags.NOPREFIX | TFFlags.WORDBREAK | TFFlags.EXPANDTABS. /// /// /// This property cannot be changed after creating OSD window. /// public TFFlags TextFormatFlags { get; set; } = TFFlags.NOPREFIX | TFFlags.WORDBREAK | TFFlags.EXPANDTABS; /// /// Icon or image at the left. Any size. /// /// /// For example System.Drawing.SystemIcons.Information or icon.stock(StockIcon.INFO) or ImageUtil.LoadGdipBitmapFromXaml("XAML copied from the Icons tool. You can edit, Width, Height and Fill (color).", screen.primary.Dpi). /// /// This property cannot be changed after creating OSD window. /// /// , or . public object Icon { get; set; } SIZE _iconSize; /// /// If true, the OSD window will have shadow. /// /// /// This property cannot be changed after creating OSD window. /// Window shadows can be disabled. See SPI_SETDROPSHADOW. /// public new bool Shadow { get => base.Shadow; set => base.Shadow = value; } /// /// If true, the OSD window receive mouse messages. Only completely transparent areas don't. The user can click to close the OSD (left, right or middle button). /// /// /// This property cannot be changed after creating OSD window. /// public new bool ClickToClose { get => base.ClickToClose; set => base.ClickToClose = value; } /// /// See . /// /// /// This property cannot be changed after creating OSD window. /// public OsdMode ShowMode { get; set; } //CONSIDER: public AnyWnd Owner { get; set; } /// public osdText() { Font = defaultSmallFont; _textColor = defaultTextColor; _backColor = defaultBackColor; _borderColor = defaultBorderColor; } /// /// Shows the OSD window. Creates if need. /// By default does not wait; the window will be closed after . /// /// /// Depending on , creates the OSD window in this or new thread. /// If the OSD window is already created, just shows it if hidden. Many properties can be changed only before creating OSD window; call if need. /// public override void Show() { if (!Hwnd.Is0) { base.Show(); return; } bool thisThread = false, wait = false; switch (ShowMode) { case OsdMode.Auto: thisThread = process.thisThreadHasMessageLoop(); break; case OsdMode.ThisThread: thisThread = true; break; case OsdMode.Wait: thisThread = wait = true; break; } if (thisThread) { _Show(wait); } else { //Task.Run(() => ShowWait()); //works too, but cannot use StrongThread run.thread(() => _Show(true), ShowMode == OsdMode.WeakThread).Name = "Au.osdText"; Au.wait.until(30, () => IsHandleCreated); //CONSIDER: make smaller timeout when main thread ended if OsdShowMode.Auto } } void _Show(bool sync) { if (!_rectIsSet) base.Rect = Measure(); base.Show(); _SetCloseTimer(); if (sync) wait.doEventsUntil(0, () => !IsHandleCreated); } void _SetCloseTimer() { int t = SecondsTimeout; if (t == 0) t = Math.Min(Text.Lenn(), 1000) / 10 + 3; //calc time from text length if (t > 0) Api.SetTimer(Hwnd, 1, Math.Min(t, int.MaxValue / 1000) * 1000, null); else Api.KillTimer(Hwnd, 1); } /// /// Close the OSD window after this time, seconds. /// If 0 (default), depends on text length. Can be (-1). /// public int SecondsTimeout { get => _secondsTimeout; set { _secondsTimeout = value; if (IsHandleCreated) Hwnd.SendNotify(Api.WM_USER + 100); } } int _secondsTimeout; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member protected override nint WndProc(wnd w, int message, nint wParam, nint lParam) { switch (message) { case Api.WM_TIMER when wParam == 1: Close(); return 0; case Api.WM_USER + 100: _SetCloseTimer(); return 0; } return base.WndProc(w, message, wParam, lParam); } #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member /// /// Draws OSD text etc. /// protected override void OnPaint(IntPtr dc, Graphics g, RECT r) { //print.it(Api.GetCurrentThreadId()); if (Opacity != 0) { g.Clear((Color)BackColor); //else OsdWindow cleared with TransparentColor if (BorderColor != BackColor) { //border g.DrawRectangleInset((Color)BorderColor, 1, r); r.Inflate(-1, -1); } } if (BackgroundImage != null) { g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; g.DrawImageUnscaledAndClipped(BackgroundImage, r); //info: if image smaller - scales; if bigger - clips. } _EnsureFontAndMargin(Dpi.OfWindow(Hwnd)); //eg if Measure not called because Rect is set r.Inflate(-_margin, -_margin); if (Font.Italic) r.Width += _margin / 2; //avoid clipping part of last char if (_iconSize != default) { int x = r.left + c_iconPadding, y = r.top + c_iconPadding; if (Icon is Image im) { g.DrawImageUnscaled(im, x, y); } else { var hi = Icon switch { icon k => k.Handle, Icon k => k.Handle, _ => default }; Api.DrawIconEx(dc, x, y, hi, 0, 0); } r.left += _iconSize.width + c_iconPadding * 2; } if (!Text.NE()) { Api.SetTextColor(dc, TextColor.ToBGR()); Api.SetBkMode(dc, 1); var tff = TextFormatFlags; if (WrapWidth > 0) tff |= TFFlags.WORDBREAK; using var soFont = new GdiSelectObject_(dc, _font); RECT rt = r; Api.DrawText(dc, Text, ref rt, tff); } } const int c_iconPadding = 5; int _margin; void _EnsureFontAndMargin(int dpi) { if (Text.NE()) { _margin = 0; } else if (_font == null) { var f = Font ?? defaultSmallFont; _font = f.CreateFont(dpi); _margin = Dpi.Scale(Math.Max(f.Size / 3, 3), dpi); } } /// /// Calculates OSD window size and position. /// Can be called before showing OSD. /// public RECT Measure() { Size z = default; if (IsOfImageSize && BackgroundImage != null) { z = BackgroundImage.Size; } else { Size zi = default; if (Icon != null) { switch (Icon) { case icon k: zi = k.Size; break; case Icon k: zi = k.Size; break; case Image k: zi = k.Size; break; } _iconSize = zi; if (zi != default) { zi.Width += c_iconPadding * 2; zi.Height += c_iconPadding * 2; } } var scrn = XY?.GetScreen() ?? defaultScreen.Now; int dpi = scrn.Dpi; _margin = 0; if (!Text.NE()) { _font?.Dispose(); _font = null; _EnsureFontAndMargin(dpi); var tff = TextFormatFlags; int maxWidth = scrn.WorkArea.Width - zi.Width - 10; int ww = WrapWidth; if (ww > 0) { maxWidth = Math.Min(maxWidth, ww); tff |= TFFlags.WORDBREAK; } using var dc = new FontDC_(_font); z = dc.MeasureDT(Text, tff, maxWidth); } z.Width += zi.Width; z.Height = Math.Max(z.Height, zi.Height); z.Width += _margin * 2; z.Height += _margin * 2; } if (Opacity != 0 && BorderColor != BackColor) { //border z.Width += 2; z.Height += 2; } RECT r = new(0, 0, z.Width, z.Height); //print.it(r); if (XY != null) { if (XY.inRect) r.MoveInRect(XY.rect, XY.x, XY.y, ensureInRect: false); else r.MoveInScreen(XY.x, XY.y, XY.screen, XY.workArea, ensureInScreen: false); r.EnsureInScreen(workArea: XY.workArea); } else { r.MoveInScreen(Coord.Center, Coord.Center, defaultScreen, workArea: true, ensureInScreen: true); } return r; } #region public static /// /// Shows a tooltip-like OSD window with text and optionally icon. /// /// Text in OSD window.
Sets . /// Sets . /// Sets . /// Sets . /// Sets . /// Sets . /// Sets . /// OSD window name.
Sets . /// Sets . /// Don't call . The caller can use the return value to set some other properties and call Show. /// Returns an object that can be used to change properties or close the OSD window. /// /// Also sets these properties: =true, =true. /// public static osdText showText(string text, int secondsTimeout = 0, PopupXY xy = null, object icon = null, ColorInt? textColor = null, ColorInt? backColor = null, FontNSS font = null, string name = null, OsdMode showMode = default, bool dontShow = false) { var o = new osdText { _text = text, SecondsTimeout = secondsTimeout, _xy = xy, Icon = icon, _textColor = textColor ?? defaultTextColor, _backColor = backColor ?? defaultBackColor, Font = font ?? defaultSmallFont, Name = name, ShowMode = showMode, ClickToClose = true, Shadow = true }; if (!dontShow) o.Show(); return o; } /// /// Shows a big-font text with transparent background. /// /// Sets . Default: . /// Sets . Default: . /// /// Also sets these properties: =0. /// /// public static osdText showTransparentText(string text, int secondsTimeout = 0, PopupXY xy = null, ColorInt? color = null, FontNSS font = null, string name = null, OsdMode showMode = default, bool dontShow = false) { var o = new osdText { _text = text, SecondsTimeout = secondsTimeout, _xy = xy, _textColor = color ?? defaultTransparentTextColor, Font = font ?? s_bigFont, Name = name, ShowMode = showMode, Opacity = 0d }; if (!dontShow) o.Show(); return o; } /// /// Shows on-screen image. /// /// Sets . /// /// Also sets these properties: =true, =0, =true. /// /// public static osdText showImage(Image image, int secondsTimeout = 0, PopupXY xy = null, string name = null, OsdMode showMode = default, bool dontShow = false) { var o = new osdText { BackgroundImage = image, SecondsTimeout = secondsTimeout, _xy = xy, Name = name, ShowMode = showMode, IsOfImageSize = true, Opacity = 0d, ClickToClose = true }; o._backColor = o.TransparentColor; if (!dontShow) o.Show(); return o; } /// Default font for and . Default: standard GUI font (usually Segoe UI), size 12. /// public static FontNSS defaultSmallFont { get => s_smallFont; set => s_smallFont = value ?? throw new ArgumentNullException(); } static FontNSS s_smallFont = new(12); /// Default font for . Default: standard GUI font (usually Segoe UI), size 24. /// public static FontNSS defaultBigFont { get => s_bigFont; set => s_bigFont = value ?? throw new ArgumentNullException(); } static FontNSS s_bigFont = new(24); /// Default text color for and . Default: 0x(dark gray). public static ColorInt defaultTextColor { get; set; } = 0x404040; /// Default border color for and . Default: 0x404040 (dark gray). public static ColorInt defaultBorderColor { get; set; } = 0x404040; /// Default background color for and . Default: 0xFFFFF0 (light yellow). public static ColorInt defaultBackColor { get; set; } = 0xFFFFF0; /// Default text color for . Default: 0x8A2BE2 (Color.BlueViolet). public static ColorInt defaultTransparentTextColor { get; set; } = 0x8A2BE2; /// /// Default screen when is not set. /// The must be lazy or empty. /// /// with Handle. Must be lazy (with ) or empty. /// /// /// public static screen defaultScreen { get => _defaultScreen; set => _defaultScreen = value.ThrowIfWithHandle_; } static screen _defaultScreen; #endregion } } namespace Au.Types { /// /// Whether waits or shows the OSD window in this or new thread. /// /// /// If this thread has windows, any value can be used, but usually Auto (default) or ThisThread is the best. /// public enum OsdMode { /// Depends on . If it is true, uses ThisThread, else StrongThread. Does not wait. Auto, /// /// Show the OSD window in this thread and don't wait. /// This thread must must be a UI thread (with windows etc). /// ThisThread, /// Show the OSD window in new thread and don't wait. Set =true, so that the OSD is closed when other threads of this app end. WeakThread, /// Show the OSD window in new thread and don't wait. Set =false, so that the OSD is not closed when other threads of this app end. StrongThread, /// /// Show the OSD window in this thread and wait until it disappears. /// Waits seconds. While waiting, dispatches messages etc; see . /// Wait, } } ================================================ FILE: Au/GUI/popupMenu/MTBase.cs ================================================ using System.Drawing; namespace Au.Types; /// /// Base class of and . /// /// /// image argument of "add item" functions can be: /// - icon name, like "*Pack.Icon color" (you can get it from the Icons tool). See . /// - file/folder path (string) - the "show" function calls to get its icon. It also supports file type icons like ".txt", etc. /// - file path with prefix "imagefile:" or resource path that starts with "resources/" or has prefix "resource:" - the "show" function loads .png or .xaml image file or resource. /// - string with prefix "image:" - Base64 encoded image file. Can be created with the Find image tool. /// - - same as folder path string. /// - - image. /// - - icon. The "add item" function disposes it. /// - - the "show" function calls . /// - null - if true, the "show" function tries to extract a file path from action code; then calls . Else no image. /// - string "" - no image, even if true. /// /// Item images should be of size 16x16 (small icon size). If high DPI, will scale images automatically, which makes them slightly blurred. To avoid scaling, can be used XAML images, but then slower. /// /// Images are loaded on demand, when showing the menu or submenu etc. If fails to load, prints warning (). /// /// For icon/image files use full path, unless they are in /// /// To add an image resource in Visual Studio, use build action Resource for the image file. /// public abstract partial class MTBase { private protected readonly string _name; private protected readonly string _sourceFile; private protected readonly int _sourceLine; private protected readonly int _threadId; private protected wnd _w; private protected int _dpi; (wnd tt, MTItem item, RECT rect) _tt; private protected MTBase() { _threadId = Api.GetCurrentThreadId(); } private protected MTBase(string name, string f_, int l_, string m_ = null) : this() { if (name == null && !m_.NE()) if (m_[0] is not ('<' or '.')) name = m_; //
$, .ctor _name = name; _sourceFile = f_; _sourceLine = l_; } private protected virtual void _WmNccreate(wnd w) { _w = w; MouseCursor.SetArrowCursor_(); //workaround for: briefly shows "wait" cursor when entering mouse first time in process } private protected virtual void _WmNcdestroy() { _w = default; _tt = default; if (_stdAO != null) { Marshal.ReleaseComObject(_stdAO); _stdAO = null; } } /// /// Extract file path or script path from item action code (for example or argument) and use icon of that file or script. /// This property is applied to items added afterwards; submenus inherit it. /// /// Default: true, with name true, without name false. /// /// Gets path from code that contains a string like @"c:\windows\system32\notepad.exe" or @"%folders.System%\notepad.exe" or URL/shell or @"\folder\script.cs". /// Also supports code patterns like folders.System + "notepad.exe", folders.shell.RecycleBin. /// /// If extracts path, also in the context menu adds item Find file which selects the file in Explorer or Open script which opens the script in editor. /// public bool ExtractIconPathFromCode { get; set; } /// /// Execute item actions asynchronously in new threads. /// This property is applied to items added afterwards; submenus inherit it. /// /// Default: true, false. /// /// If current thread is a UI thread (has windows etc) or has triggers or hooks, and item action functions execute some long automations etc in current thread, current thread probably is hung during that time. Set this property = true to avoid it. /// public bool ActionThread { get; set; } /// /// Whether to handle exceptions in item action code. If false (default), handles exceptions and on exception calls . /// This property is applied to items added afterwards; submenus inherit it. /// /// Default: false, false. public bool ActionException { get; set; } /// /// If an item has file path, show it in tooltip. /// This property is applied to items added afterwards; submenus inherit it. /// /// Default: false, false. public bool PathInTooltip { get; set; } /// /// Width and height of images. Default 16, valid 16-256. /// public int ImageSize { get; set; } = 16; private protected IconImageCache _ImageCache => field ??= IconImageCache.CommonOfSize(ImageSize); private protected void _CopyProps(MTBase m) { m.ImageSize = ImageSize; m.ActionException = ActionException; m.ActionThread = ActionThread; m.ExtractIconPathFromCode = ExtractIconPathFromCode; m.PathInTooltip = PathInTooltip; } private protected string _SourceLink(MTItem x, string text) => x.sourceFile == null ? null : $"{text}<>"; private protected bool _IsOtherThread => _threadId != Api.GetCurrentThreadId(); internal void _ThreadTrap() { if (_threadId != Api.GetCurrentThreadId()) throw new InvalidOperationException("Wrong thread."); } /// /// Converts x.image (object containing string, Image, etc or null) to Image. Extracts icon path from code if need. Returns default if will extract async. /// private protected (Image image, bool dispose) _GetImage(MTItem x) { Image im = null; bool dontDispose = false; if (x.extractIconPath == 1) { //extract path always, not only when x.image==null, or we would not have path for other purposes x.file = icon.ExtractIconPathFromCode_(x.clicked.Method, out bool cs); if (x.file != null) { x.image ??= x.file; x.extractIconPath = (byte)(cs ? 4 : 2); } else x.extractIconPath = 3; } switch (x.image) { case Image g: im = g; dontDispose = true; break; case string s when s.Length > 0: try { dontDispose = true; bool isImage = ImageUtil.HasImageOrResourcePrefix(s); im = _ImageCache.Get(s, _dpi, isImage, _OnException); if (im == null && isImage) _OnException(s, null); } catch (Exception e1) { _OnException(null, e1); } void _OnException(string s, Exception e) { print.it($"<>Failed to load image. {e?.ToStringWithoutStack().TrimEnd('.') ?? s}. {_SourceLink(x, "Edit")}"); } break; case StockIcon si: im = icon.stock(si)?.ToGdipBitmap(); break; } return (im, im != null && !dontDispose); } private protected string _GetFullTooltip(MTItem b) { var s = b.Tooltip; if (this is toolbar tb) { var v = b as TBItem; if (!(tb.DisplayText || v.textAlways)) { if (s.NE()) s = v.Text; else if (!v.Text.NE()) s = b.Text + "\n" + s; } } if (b.pathInTooltip) { var sf = b.File; if (!(sf.NE() || sf.Starts("::") || sf.Starts("shell:"))) s = s.NE() ? sf : s + "\n" + sf; } return s; } private protected unsafe void _SetTooltip(MTItem b, RECT r, nint lParam, int submenuDelay = -1) { string s = _GetFullTooltip(b); bool setTT = !s.NE() && b != _tt.item; if (!setTT && (setTT = _tt.item != null && _tt.item.rect != _tt.rect)) b = _tt.item; //update tooltip tool rect if (setTT) { _tt.rect = r; _tt.item = b; if (!_tt.tt.IsAlive) { _tt.tt = Api.CreateWindowEx(WSE.TOPMOST | WSE.TRANSPARENT, "tooltips_class32", null, Api.TTS_ALWAYSTIP | Api.TTS_NOPREFIX, 0, 0, 0, 0, _w); _tt.tt.Send(Api.TTM_ACTIVATE, 1); _tt.tt.Send(Api.TTM_SETMAXTIPWIDTH, 0, screen.of(_w).WorkArea.Width / 3); } if (b is PMItem) { //ensure the tooltip is above submenu in Z order _tt.tt.ZorderTopRaw_(); if (submenuDelay > 0) submenuDelay += 100; _tt.tt.Send(0x403, 3, submenuDelay > (int)_tt.tt.Send(0x415, 3) ? submenuDelay : -1); //TTM_SETDELAYTIME,TTM_GETDELAYTIME,TTDT_INITIAL } fixed (char* ps = s) { var g = new Api.TTTOOLINFO { cbSize = sizeof(Api.TTTOOLINFO), hwnd = _w, uId = 1, lpszText = ps, rect = r }; _tt.tt.Send(Api.TTM_DELTOOL, 0, &g); _tt.tt.Send(Api.TTM_ADDTOOL, 0, &g); } } else { if (b != _tt.item) _HideTooltip(); } if (_tt.item != null) { var v = new MSG { hwnd = _w, message = Api.WM_MOUSEMOVE, lParam = lParam }; _tt.tt.Send(Api.TTM_RELAYEVENT, 0, &v); } } private protected unsafe void _HideTooltip() { if (_tt.item != null) { _tt.item = null; var g = new Api.TTTOOLINFO { cbSize = sizeof(Api.TTTOOLINFO), hwnd = _w, uId = 1 }; _tt.tt.Send(Api.TTM_DELTOOL, 0, &g); } } } /// /// Base of etc. /// public abstract class MTItem { internal Delegate clicked; internal object image; /// 1 if need to extract, 2 if already extracted (the image field is the path), 3 if failed to extract, 4 if extracted "script.cs" internal byte extractIconPath; //from MTBase.ExtractIconPathFromCode internal bool actionThread; //from MTBase.ActionThread internal bool actionException; //from MTBase.ActionException internal bool pathInTooltip; //from MTBase.PathInTooltip internal int sourceLine; internal string sourceFile; internal string file; internal RECT rect; internal Image image2; internal bool HasImage_ => image2 != null; /// /// Item text. /// public string Text { get; set; } /// /// Item tooltip. /// public string Tooltip { get; set; } /// /// Any value. Not used by this library. /// public object Tag { get; set; } /// public ColorInt TextColor { get; set; } /// /// Gets file or script path extracted from item action code (see ) or sets path as it would be extracted. /// /// /// Can be used to set file or script path when it cannot be extracted from action code. /// When you set this property, the menu/toolbar item uses icon of the specified file, and its context menu contains Find file or Open script. /// public string File { get => file; set { file = value; if (file == null) { extractIconPath = 3; } else { image ??= file; bool cs = file.Ends(".cs", true) && !pathname.isFullPath(file, orEnvVar: true); extractIconPath = (byte)(cs ? 4 : 2); } } } internal void GoToFile_() { if (file.NE()) return; if (extractIconPath == 2) run.selectInExplorer(file); else ScriptEditor.Open(file); } internal static (bool edit, bool go, string goText) CanEditOrGoToFile_(string sourceFile, MTItem item) { if (sourceFile != null) { if (ScriptEditor.Available) { if (item?.file == null) return (true, false, null); return (true, true, item.extractIconPath == 2 ? "Find file" : "Open script"); } else if (item?.extractIconPath == 2) { return (false, true, "Find file"); } } return default; } /// /// Call when adding menu/toolbar item. /// Sets text and tooltip (from text). Sets clicked, image and sourceLine fields. /// Sets extractIconPath, actionThread and actionException fields from mt properties. /// internal void Set_(MTBase mt, string text, Delegate click, MTImage im, int l_, string f_) { if (!text.NE()) { var mi = this as PMItem; bool rawText = mi?.rawText ?? false; int i = text.IndexOf('\0'); if (i < 0 && !rawText) i = text.IndexOf('|'); if (i >= 0) { var v = _Split(text, i); text = v.Item1; Tooltip = v.Item2; } int len = text.Lenn(); if (len > 0 && text[^1] == '\a') { text = text[..^1]; //remove for menu too, because user may move items from toolbar to menu and forget to remove '\a' len--; if (this is TBItem ti) ti.textAlways = true; //note: textAlways of groups is already true } if (mi != null && !rawText && len > 1) { i = text.IndexOf('\t', 1); if (i > 0) { var v = _Split(text, i); text = v.Item1; mi.Hotkey = v.Item2; } } if (!text.NE()) Text = text; static (string, string) _Split(string s, int i) { int j = i + 1; if (s.Eq(j, ' ')) j++; return (i > 0 ? s[..i] : null, j < s.Length ? s[j..] : null); } } image = im.Value; if (image is icon ic) { image = ic.ToGdipBitmap(); image ??= ""; } //DestroyIcon now; don't extract from code. clicked = click; sourceLine = l_; sourceFile = f_; extractIconPath = (byte)((mt.ExtractIconPathFromCode && clicked is not (null or Action or Func)) ? 1 : 0); actionThread = mt.ActionThread; actionException = mt.ActionException; pathInTooltip = mt.PathInTooltip; } /// public override string ToString() => Text; } /// /// Used for menu/toolbar function parameters to specify an image in different ways (file path, object, etc). /// /// /// Has implicit conversions from string, , , , . /// More info: . /// public struct MTImage { readonly object _o; MTImage(object o) { _o = o; } /// public static implicit operator MTImage(string pathEtc) => new(pathEtc); /// public static implicit operator MTImage(Image image) => new(image); /// public static implicit operator MTImage(icon icon) => new(icon); /// public static implicit operator MTImage(StockIcon icon) => new(icon); /// public static implicit operator MTImage(FolderPath path) => new((string)path); /// /// Gets the raw value stored in this variable. Can be string, , , or null. /// public object Value => _o; } ================================================ FILE: Au/GUI/popupMenu/pm acc.cs ================================================ using IAccessible = Au.Types.Api.IAccessible; using VarInt = Au.Types.Api.VarInt; using NAVDIR = Au.Types.Api.NAVDIR; namespace Au.Types { [ComVisible(true)] partial class MTBase { private protected bool _WmGetobject(nint wParam, nint lParam, out nint result) { result = default; var oid = (EObjid)lParam; if (oid != EObjid.CLIENT) return false; result = Cpp.Cpp_AccWorkaround((IAccessible)this, wParam, ref _accWorkaround); return true; } nint _accWorkaround; /// ~MTBase() { if (_accWorkaround != 0) Cpp.Cpp_AccWorkaround(null, 0, ref _accWorkaround); } private protected IAccessible _StdAO { get { if (_stdAO == null) Api.CreateStdAccessibleObject(_w, EObjid.CLIENT, typeof(IAccessible).GUID, out _stdAO); return _stdAO; } } IAccessible _stdAO; } } namespace Au { [ComVisible(true)] partial class popupMenu : IAccessible { IAccessible IAccessible.get_accParent() => _StdAO.get_accParent(); int IAccessible.get_accChildCount() => _a.Count; int IAccessible.get_accChild(VarInt varChild, out object ppdispChild) { ppdispChild = null; return 1; } string IAccessible.get_accName(VarInt varChild) => !_B(varChild, out var b) ? _name : (b.rawText ? b.Text : StringUtil.RemoveUnderlineChar(b.Text)); string IAccessible.get_accValue(VarInt varChild) => null; string IAccessible.get_accDescription(VarInt varChild) => _B(varChild, out _) ? null : "Popup menu"; VarInt IAccessible.get_accRole(VarInt varChild) { var r = !_B(varChild, out var b) ? ERole.MENUPOPUP : (b.IsSeparator ? ERole.SEPARATOR : ERole.MENUITEM); return (int)r - 1; } VarInt IAccessible.get_accState(VarInt varChild) { EState r = 0; if (!_w.IsEnabled()) r |= EState.DISABLED; if (!_B(varChild, out var b)) { if (!_w.IsVisible) r |= EState.INVISIBLE; } else { if (b.IsDisabled) r |= EState.DISABLED; if (FocusedItem == b) r |= EState.FOCUSED | EState.HOTTRACKED; if (b.IsChecked) r |= EState.CHECKED; if (b.IsSubmenu) r |= EState.HASPOPUP; //TODO3: if offscreen, r |= EState.INVISIBLE | EState.OFFSCREEN; } return (int)r - 1; } string IAccessible.get_accHelp(VarInt varChild) => _B(varChild, out var b) ? _GetFullTooltip(b) : null; int IAccessible.get_accHelpTopic(out string pszHelpFile, VarInt varChild) => throw new NotImplementedException(); string IAccessible.get_accKeyboardShortcut(VarInt varChild) { if (_B(varChild, out var b) && !b.rawText) { var s = b.Text; int i = StringUtil.FindUnderlineChar(s); if (i >= 0) return s[i].ToString(); } return null; } object IAccessible.get_accFocus() => null; object IAccessible.get_accSelection() => null; string IAccessible.get_accDefaultAction(VarInt varChild) => !_B(varChild, out var b) || b.IsDisabled ? null : b.IsSubmenu ? "Open" : "Execute"; void IAccessible.accSelect(ESelect flagsSelect, VarInt varChild) => throw new NotImplementedException(); void IAccessible.accLocation(out int pxLeft, out int pyTop, out int pcxWidth, out int pcyHeight, VarInt varChild) { if (!_B(varChild, out var b)) { _StdAO.accLocation(out pxLeft, out pyTop, out pcxWidth, out pcyHeight, varChild); } else { var r = _ItemRect(b, inScreen: true); pxLeft = r.left; pyTop = r.top; pcxWidth = r.Width; pcyHeight = r.Height; } } object IAccessible.accNavigate(NAVDIR navDir, VarInt varStart) { int i = varStart; if (navDir == NAVDIR.FIRSTCHILD || navDir == NAVDIR.LASTCHILD) { if (i == -1) return navDir == NAVDIR.FIRSTCHILD ? 1 : _a.Count; } else { if (i == -1) return _StdAO.accNavigate(navDir, varStart); switch (navDir) { case NAVDIR.PREVIOUS: if (i > 0) return i; break; case NAVDIR.NEXT: if (++i < _a.Count) return i + 1; break; } } return null; } VarInt IAccessible.accHitTest(int xLeft, int yTop) { POINT p = new(xLeft, yTop); _w.MapScreenToClient(ref p); if (!_w.ClientRect.Contains(p)) return _StdAO.accHitTest(xLeft, yTop); return _HitTest(p); } void IAccessible.accDoDefaultAction(VarInt varChild) { if (!_B(varChild, out var b) || b.IsDisabled) return; _w.Post(Api.WM_USER + 50, (int)varChild); } void IAccessible.put_accName(VarInt varChild, string szName) => throw new NotImplementedException(); void IAccessible.put_accValue(VarInt varChild, string szValue) => throw new NotImplementedException(); bool _B(VarInt varChild, out PMItem b) { int i = varChild; if (i == -1) { b = null; return false; } b = _a[i]; return true; } } } ================================================ FILE: Au/GUI/popupMenu/pm render.cs ================================================ using System.Drawing; using System.Drawing.Drawing2D; namespace Au; public unsafe partial class popupMenu { /// /// Sets some metrics, for example item padding. /// /// public PMMetrics Metrics { get; set; } /// /// Sets or gets default metrics. /// /// public static PMMetrics defaultMetrics { get => s_defaultMetrics ??= new(); set { s_defaultMetrics = value; } } static PMMetrics s_defaultMetrics; /// /// Sets or gets font. /// public FontNSS Font { get; set; } /// /// Sets or gets default font. /// public static FontNSS defaultFont { get; set; } NativeFont_ _GetFont(bool bold = false) { var font = Font ?? defaultFont; if (font == null) return bold ? NativeFont_.BoldCached(_dpi) : NativeFont_.RegularCached(_dpi); if (!bold) return _font ??= font.CreateFont(_dpi); return _fontBold ??= (font with { Bold = true }).CreateFont(_dpi); } NativeFont_ _font, _fontBold; //disposing in _WmNcdestroy or ~NativeFont_ _Metrics _met; class _Metrics : IDisposable { public bool hasImages, hasSubmenus, hasSeparators, hasHotkeys, checkInImageColumn; public int border, paddingY, paddingLeft, paddingRight, textPaddingX, textPaddingY, image, check, check2, submenu, submenuMargin, separator, sepLine; public SIZE sizeCheck, sizeSubmenu; public IntPtr theme; public int xTextEnd, xHotkeyStart; public void Dispose() { if (theme != default) { Api.CloseThemeData(theme); theme = default; } GC.SuppressFinalize(this); } ~_Metrics() => Dispose(); public _Metrics(popupMenu m) { bool hasCheck = false; foreach (var b in m._a) { if (b.IsSeparator) hasSeparators = true; else { if (b.HasImage_) hasImages = true; if (b.checkType > 0) hasCheck = true; if (b.IsSubmenu) hasSubmenus = true; if (b.Hotkey != null) hasHotkeys = true; } } if (hasCheck && hasImages) { checkInImageColumn = true; foreach (var b in m._a) if (b.checkType > 0 && b.HasImage_) { checkInImageColumn = false; break; } } var k = m.Metrics; if (k == null) { for (var p = m._sub.parent; p != null; p = p._sub.parent) if ((k = p.Metrics) != null) break; } k ??= defaultMetrics; int dpi = m._dpi; border = dpi / 96; paddingY = Dpi.Scale(k.ItemPaddingY, dpi); paddingLeft = Dpi.Scale(k.ItemPaddingLeft, dpi); paddingRight = Dpi.Scale(k.ItemPaddingRight, dpi); textPaddingX = Dpi.Scale(8, dpi); textPaddingY = Dpi.Scale(1, dpi); if (hasImages) image = Dpi.Scale(m.ImageSize, dpi); if (hasCheck) check = Dpi.Scale(18, dpi); if (hasSubmenus) submenu = Dpi.Scale(16, dpi); separator = Dpi.Scale(8, dpi); sepLine = 2; theme = Api.OpenThemeData(m._w, "Menu", dpi); if (theme != default) { using var dc = new ScreenDC_(); if (hasSubmenus) { Api.GetThemePartSize(theme, dc, 16, 1, null, Api.THEMESIZE.TS_TRUE, out sizeSubmenu); submenu = Math.Max(submenu, sizeSubmenu.width + submenu / 4); } if (hasCheck) { Api.GetThemePartSize(theme, dc, 11, 1, null, Api.THEMESIZE.TS_TRUE, out sizeCheck); check = Math.Max(check, sizeCheck.width + 2); } if (hasSeparators && 0 == Api.GetThemePartSize(theme, dc, 15, 0, null, Api.THEMESIZE.TS_TRUE, out var z)) sepLine = z.height; } if (checkInImageColumn) { check2 = check; check = 0; } submenuMargin = hasHotkeys ? 0 : submenu; } } SIZE _Measure(int maxWidth) { SIZE R = default; _met?.Dispose(); _met = new _Metrics(this); int buttonPlusX = (_met.border + _met.textPaddingX) * 2 + _met.check + _met.image + _met.submenu + _met.submenuMargin + _met.paddingLeft + _met.paddingRight; int maxTextWidth = maxWidth - buttonPlusX; var font = _GetFont(); using var dc = new FontDC_(font); int textHeight = dc.MeasureEP(" ").height; int maxHotkey = 0; if (_met.hasHotkeys) { foreach (var b in _a) { if (b.Hotkey == null) continue; int wid = dc.MeasureDT(b.Hotkey, c_tffHotkey).width; maxHotkey = Math.Max(maxHotkey, Math.Min(wid, maxTextWidth / 2)); } } int hotkeyPlus = textHeight * 3 / 2; //space between text and hotkey if (maxHotkey > 0) maxTextWidth -= maxHotkey += hotkeyPlus; int y = 0; for (int i = 0; i < _a.Count; i++) { //note: to support multiline, wrap, underlines and tabs we have to use DrawText, not TextOut. // DrawText(DT_CALCRECT) is slow. Very slow compared with GetTextExtentPoint32. Eg 100-300 ms for 1000 items. Depends on text length. var b = _a[i]; SIZE z; if (b.IsSeparator) { z = new(0, _met.separator); } else { var s = b.Text; if (!s.NE()) { if (b.FontBold) Api.SelectObject(dc, _GetFont(true)); z = dc.MeasureDT(s, _TfFlags(b), maxTextWidth); z.width = Math.Min(z.width, maxTextWidth); if (b.FontBold) Api.SelectObject(dc, font); _met.xTextEnd = Math.Max(_met.xTextEnd, z.width); z.width += buttonPlusX; b.textHeight = z.height; } else z = new(0, textHeight); z.height = Math.Max(z.height + _met.textPaddingY * 2 + 1, _met.image + _met.image / 16 * 2) + (_met.border + _met.paddingY) * 2; } b.rect = new(0, y, z.width, z.height); y += z.height; R.width = Math.Max(R.width, z.width); R.height = Math.Max(R.height, y); } if (maxHotkey > 0) { R.width += maxHotkey; _met.xHotkeyStart = _met.xTextEnd + hotkeyPlus; } foreach (var b in _a) b.rect.right = R.width; return R; } TFFlags _TfFlags(PMItem b) { var f = c_tff; if (b.rawText) f |= TFFlags.NOPREFIX; else if (!_flags.Has(PMFlags.Underline)) f |= TFFlags.HIDEPREFIX; return f; } const TFFlags c_tff = TFFlags.EXPANDTABS | TFFlags.WORDBREAK /*| TFFlags.PATH_ELLIPSIS*/; const TFFlags c_tffHotkey = TFFlags.NOPREFIX | TFFlags.SINGLELINE | TFFlags.VCENTER; void _Render(IntPtr dc, RECT rUpdate) { using (var menuBrush = GdiObject_.SysColorBrush(_met.theme, Api.COLOR_BTNFACE)) menuBrush.BrushFill(dc, rUpdate); using var g = _met.hasImages ? Graphics.FromHdc(dc) : null; if (g != null) { g.InterpolationMode = InterpolationMode.HighQualityBicubic; } var font = _GetFont(); using var soFont = new GdiSelectObject_(dc, font); Api.SetBkMode(dc, 1); int textColor = Api.GetThemeSysColor(_met.theme, Api.COLOR_BTNTEXT), textColorDisabled = Api.GetThemeSysColor(_met.theme, Api.COLOR_GRAYTEXT); rUpdate.Offset(0, _scroll.Offset); for (int i = _scroll.Pos; i < _a.Count; i++) { var b = _a[i]; if (b.rect.bottom <= rUpdate.top) continue; if (b.rect.top >= rUpdate.bottom) break; var r = _ItemRect(b); if (b.IsSeparator) { r.Inflate(-_met.textPaddingX / 4, 0); //r.left+=_met.check+_met.image; r.top += (r.Height - _met.sepLine) / 2; r.Height = _met.sepLine; if (_met.theme != default) Api.DrawThemeBackground(_met.theme, dc, 15, 0, r); else Api.DrawEdge(dc, ref r, Api.EDGE_ETCHED, Api.BF_TOP); } else { if (b.BackgroundColor != default) using (var brush = GdiObject_.ColorBrush(b.BackgroundColor)) brush.BrushFill(dc, r); if (i == _iHot) { if ((textColor == 0 || b.TextColor != default) && b.BackgroundColor == default) using (var brush = GdiObject_.ColorBrush(0xC0DCF3)) brush.BrushFill(dc, r); using (var brush = GdiObject_.ColorBrush(0x90C8F6)) brush.BrushRect(dc, r); } r.Inflate(-_met.border, -_met.border - _met.paddingY); r.left += _met.paddingLeft; r.right -= _met.paddingRight; var r2 = r; r.left += _met.check; if (b.HasImage_) { try { g.DrawImage(b.image2, r.left + _met.textPaddingY, r.top + (r.Height - _met.image) / 2, _met.image, _met.image); } catch (Exception ex) { Debug_.Print(ex); } } r.left += _met.image + _met.textPaddingX; r.right -= _met.textPaddingX + _met.submenu + _met.submenuMargin; r.top += _met.textPaddingY; r.bottom -= _met.textPaddingY; if (b.Hotkey != null) { Api.SetTextColor(dc, textColorDisabled); var rr = r; rr.left += _met.xHotkeyStart; Api.DrawText(dc, b.Hotkey, ref rr, c_tffHotkey); } Api.SetTextColor(dc, b.TextColor != default ? b.TextColor.ToBGR() : (b.IsDisabled ? textColorDisabled : textColor)); if (!b.Text.NE()) { if (b.FontBold) Api.SelectObject(dc, _GetFont(true)); r.Width = _met.xTextEnd; var rr = r; rr.Offset(0, (r.Height - b.textHeight) / 2); //vcenter Api.DrawText(dc, b.Text, ref rr, _TfFlags(b)); if (b.FontBold) Api.SelectObject(dc, font); } if (b.IsSubmenu) { _DrawControl(_met.sizeSubmenu, 16, b.IsDisabled ? 2 : 1, "➜", r2.right - _met.submenu, r.top, _met.submenu, r.Height); } if (b.IsChecked) { _DrawControl(_met.sizeCheck, 11, b.checkType == 1 ? (b.IsDisabled ? 2 : 1) : (b.IsDisabled ? 4 : 3), b.checkType == 1 ? "✔" : "●", r2.left, r.top, Math.Max(_met.check, _met.check2), r.Height); } void _DrawControl(SIZE z, int part, int state, string c, int x, int y, int width, int height) { if (_met.theme != default && z != default) { RECT r = new(x, r2.top + (r2.Height - z.height) / 2, z.width, z.height); //vcenter Api.DrawThemeBackground(_met.theme, dc, part, state, r); } else { RECT r = new(x, y, width, height); Api.DrawText(dc, c, ref r, TFFlags.CENTER | TFFlags.VCENTER | TFFlags.SINGLELINE | TFFlags.NOCLIP); //cannot use DrawFrameControl(DFC_MENU, DFCS_MENUARROW etc), it draws with white background and small when high DPI } } } } } void _WmNcpaint() { using var dc = new WindowDC_(Api.GetWindowDC(_w), _w); RECT r = new(0, 0, _size.window.width, _size.window.height); using var brushBorder = GdiObject_.SysColorBrush(_met.theme, Api.COLOR_BTNSHADOW); using var brushPadding = GdiObject_.SysColorBrush(_met.theme, Api.COLOR_BTNFACE); for (int i = 0; i < _size.border; i++) { Api.FrameRect(dc, r, i == 0 ? brushBorder : brushPadding); r.Inflate(-1, -1); } } internal void Invalidate_(PMItem k = null) { _ThreadTrap(); if (_w.Is0) return; if (k != null) Api.InvalidateRect(_w, _ItemRect(k)); else Api.InvalidateRect(_w); } void _Invalidate(int i) => Invalidate_(_a[i]); void _Images() { foreach (var v in _a) { v.image2 = _GetImage(v).image; } } //not used //internal void ChangeImage_(PMItem ti, Bitmap b) { // ti.image2 = b; // _Invalidate(ti); //} } ================================================ FILE: Au/GUI/popupMenu/pm types.cs ================================================ namespace Au.Types; /// /// Represents a menu item in . /// /// /// Most properties cannot be changed while the menu is open. Can be changed , , and . /// public class PMItem : MTItem { readonly popupMenu _m; internal byte checkType; //1 checkbox, 2 radio internal bool checkDontClose; internal bool rawText; internal int textHeight; internal PMItem(popupMenu m, bool isDisabled, bool isChecked = false) { _m = m; _isDisabled = isDisabled; _isChecked = isChecked; checkDontClose = m.CheckDontClose; rawText = m.RawText; } /// Gets item action. public Action Clicked => base.clicked as Action; /// Gets or sets menu item id. public int Id { get; set; } /// true if is a submenu-item. public bool IsSubmenu { get; init; } /// true if is a separator. public bool IsSeparator { get; init; } /// /// Gets or sets disabled state. /// public bool IsDisabled { get => _isDisabled || IsSeparator; set { if (value != _isDisabled && !IsSeparator) { _isDisabled = value; _m.Invalidate_(this); } } } bool _isDisabled; /// /// Gets or sets checked state. /// /// The set function throws this exception if the item isn't checkable. Use or . public bool IsChecked { get => _isChecked; set { if (checkType == 0) throw new InvalidOperationException(); if (value != _isChecked) { _isChecked = value; _m.Invalidate_(this); } } } bool _isChecked; /// Gets or sets whether to use bold font. public bool FontBold { get; set; } /// Gets or sets background color. public ColorInt BackgroundColor { get; set; } /// /// Hotkey display text. /// public string Hotkey { get; set; } /// /// Invokes if not null. /// Handles exceptions if need. Invokes in new thread if need. /// internal void InvokeAction_() { if (clicked is Action action) { if (actionThread) run.thread(() => _Invoke(), background: false); else _Invoke(); void _Invoke() { try { action(this); } catch (Exception ex) when (!this.actionException) { print.warning(ex); } } } } } /// /// Flags for ShowX methods. /// /// /// The AlignX flags are for API TrackPopupMenuEx. /// [Flags] public enum PMFlags { /// Show by the caret (text cursor) position. If not possible, use flag WindowCenter or ScreenCenter or xy or mouse position. ByCaret = 0x1000000, /// Show in the center of the screen that contains the mouse pointer. ScreenCenter = 0x2000000, /// Show in the center of the active window. WindowCenter = 0x4000000, /// Underline characters preceded by &, regardless of Windows settings. More info: . Underline = 0x8000000, //TPM_ flags /// Horizontally align the menu so that the show position would be in its center. AlignCenterH = 0x4, /// Horizontally align the menu so that the show position would be at its right side. AlignRight = 0x8, /// Vertically align the menu so that the show position would be in its center. AlignCenterV = 0x10, /// Vertically align the menu so that the show position would be at its bottom. AlignBottom = 0x20, /// Show at the bottom or top of excludeRect, not at the right/left. AlignRectBottomTop = 0x40, } /// /// Used with . /// public enum PMKHook { /// Process the key event as usually. Default, /// Close the menu. Close, /// Do nothing. None, /// Execute the focused item and close the menu. ExecuteFocused, } /// /// Used with and . /// /// /// All values are in logical pixels (1 pixel when DPI is 100%). /// public record class PMMetrics(int ItemPaddingY = 0, int ItemPaddingLeft = 0, int ItemPaddingRight = 0); ================================================ FILE: Au/GUI/popupMenu/popupMenu.cs ================================================ //TODO3: winevents EEvent.SYSTEM_MENUSTART, EEvent.SYSTEM_MENUEND, EEvent.SYSTEM_MENUPOPUPSTART, EEvent.SYSTEM_MENUPOPUPEND namespace Au; /// /// Popup menu. /// /// /// Can be used everywhere: in automation scripts, WPF apps, other apps, etc. /// Also can be used as a popup list and supports many items with scrollbar. /// /// Menu item text can include hotkey after '\t' character and/or tooltip after '|' or '\0' character. Examples: "Text\t Hotkey", "Text|Tooltip", "Text\t Hotkey\0 Tooltip". Character with prefix & (eg 'A' in "Save &As") will be underlined (depends on Windows settings and ) and can be used to select the item with keyboard. /// /// Keyboard, mouse: /// - Enter, Tab, Space - close the menu and execute the focused item. Or show the submenu. /// - Esc - close the menu or current submenu. /// - Left - close current submenu. /// - Right - open submenu. /// - Down, Up, PageDown, PageUp, End, Home - focus other item. /// - underlined menu item character - close the menu and execute the item. Or show the submenu. See . /// - Alt, Win, F10, Apps, Back - close menus. /// - click outside - close the menu. /// - middle click - close the menu. /// - right click - show context menu (if used constructor with parameters). /// /// While a menu is open, it captures many keyboard keys, even when its thread isn't the foreground thread. /// /// Not thread-safe. All functions must be called in same thread, unless documented otherwise. /// /// /// print.it(o); /// m["Two\0Tooltip", image: icon.stock(StockIcon.DELETE)] = o => { print.it(o); dialog.show(o.ToString()); }; /// m.Submenu("Submenu", m => { /// m["Three"] = o => print.it(o); /// m["Four"] = o => print.it(o); /// }); /// m["notepad"] = o => run.itSafe(folders.System + "notepad.exe"); /// m.Show(); /// ]]> /// public unsafe partial class popupMenu : MTBase { readonly List _a = new(); int _lastId; //to auto-generate item ids bool _addedNewItems; (SIZE window, SIZE client, int border) _size; NativeScrollbar_ _scroll; (popupMenu child, popupMenu parent, PMItem item, timer timer) _sub; (POINT p, bool track, bool left, bool right, bool middle) _mouse; int _iHot = -1; PMFlags _flags; PMItem _result; static popupMenu() { WndUtil.RegisterWindowClass("Au.popupMenu", etc: new() { style = Api.CS_HREDRAW | Api.CS_VREDRAW | Api.CS_DROPSHADOW, mCursor = MCursor.Arrow }); } /// /// Use this constructor for various context menus of your app. /// /// /// Users cannot right-click a menu item and open/select it in editor. /// public popupMenu() { } /// /// Use this constructor in scripts. /// /// Menu name. Must be a unique valid filename. Currently not used. Can be null. /// [](xref:caller_info) /// [](xref:caller_info) /// /// This overload sets = true. /// /// Users can right-click an item to open/select it in editor, unless f_ is explicitly set = null. /// public popupMenu(string name, [CallerFilePath] string f_ = null, [CallerLineNumber] int l_ = 0) : base(name, f_, l_) { ExtractIconPathFromCode = true; } #region add PMItem _Add(PMItem mi, string text, MTImage image, int l_, string f_, Delegate click = null) { _ThreadTrap(); _OpenTrap("cannot add items while the menu is open. To add to submenu, use the submenu variable."); if (!mi.IsSeparator) mi.Set_(this, text, click, image, l_, _sourceFile == null ? null : f_); _a.Add(mi); _addedNewItems = true; Last = mi; return mi; } /// /// Adds menu item with explicitly specified id. /// /// Item id that will return if clicked this item. /// Item text. Can include hotkey, tooltip and underlined character, like "Te&xt\t Hotkey\0 Tooltip"; more info: . /// Item image. Read here: . /// Disabled state. /// [](xref:caller_info) /// [](xref:caller_info) public PMItem Add(int id, string text, MTImage image = default, bool disable = false, [CallerLineNumber] int l_ = 0, [CallerFilePath] string f_ = null) => _Add(new PMItem(this, disable) { Id = _lastId = id }, text, image, l_, f_); /// /// Adds menu item with auto-generated id. /// /// Item text. Can include hotkey, tooltip and underlined character, like "Te&xt\t Hotkey\0 Tooltip"; more info: . /// Item image. Read here: . /// Disabled state. /// [](xref:caller_info) /// [](xref:caller_info) /// /// Assigns id = the last specified or auto-generated id + 1. If not using explicitly specified ids, auto-generated ids are 1, 2, 3... Submenu-items, separators and items with action don't auto-generate ids. /// public PMItem Add(string text, MTImage image = default, bool disable = false, [CallerLineNumber] int l_ = 0, [CallerFilePath] string f_ = null) => _Add(new PMItem(this, disable) { Id = ++_lastId }, text, image, l_, f_); /// /// Adds menu item with action (callback function) that is executed on click. /// /// Item text. Can include hotkey, tooltip and underlined character, like "Te&xt\t Hotkey\0 Tooltip"; more info: . /// Action executed on click. /// Item image. Read here: . /// Disabled state. /// [](xref:caller_info) /// [](xref:caller_info) /// /// This function is the same as the indexer. The difference is, Add returns object of the added item. When using the indexer, to access the item use . These codes are the same: var v=m.Add("text", o=>{});" and m["text"]=o=>{}; var v=m.Last;. /// public PMItem Add(string text, Action click, MTImage image = default, bool disable = false, [CallerLineNumber] int l_ = 0, [CallerFilePath] string f_ = null) => _Add(new PMItem(this, disable), text, image, l_, f_, click); /// /// Adds menu item with action (callback function) that is executed on click. /// /// Item text. Can include hotkey, tooltip and underlined character, like "Te&xt\t Hotkey\0 Tooltip"; more info: . /// Item image. Read here: . /// Disabled state. /// [](xref:caller_info) /// [](xref:caller_info) /// Action executed on click. Can be null. /// /// This function is the same as . The difference is, Add returns object of the added item. When using the indexer, to access the item use . These codes are the same: var v=m.Add("text", o=>{});" and m["text"]=o=>{}; var v=m.Last;. /// public Action this[string text, MTImage image = default, bool disable = false, [CallerLineNumber] int l_ = 0, [CallerFilePath] string f_ = null] { set { Add(text, value, image, disable, l_, f_); } } /// /// Adds menu item to be used as a checkbox. /// /// Item text. Can include hotkey, tooltip and underlined character, like "Te&xt\t Hotkey\0 Tooltip"; more info: . /// Checked state. /// Action executed on click. /// Disabled state. /// Item image. Read here: . /// [](xref:caller_info) /// [](xref:caller_info) /// /// When clicked, state is changed. /// public PMItem AddCheck(string text, bool check = false, Action click = null, bool disable = false, MTImage image = default, [CallerLineNumber] int l_ = 0, [CallerFilePath] string f_ = null) => _Add(new PMItem(this, disable, check) { checkType = 1 }, text, image, l_, f_, click); /// /// Adds menu item to be used as a radio button in a group of such items. /// /// Item text. Can include hotkey, tooltip and underlined character, like "Te&xt\t Hotkey\0 Tooltip"; more info: . /// Checked state. /// Action executed on click. /// Disabled state. /// Item image. Read here: . /// [](xref:caller_info) /// [](xref:caller_info) /// /// When clicked an unchecked radio item, its state becomes true; IsChecked of other group items become false. /// public PMItem AddRadio(string text, bool check = false, Action click = null, bool disable = false, MTImage image = default, [CallerLineNumber] int l_ = 0, [CallerFilePath] string f_ = null) => _Add(new PMItem(this, disable, check) { checkType = 2 }, text, image, l_, f_, click); /// /// Adds menu item that opens a submenu. /// Used like m.Submenu("Example", m => { /* add submenu items */ });. /// /// Item text. Can include hotkey, tooltip and underlined character, like "Te&xt\t Hotkey\0 Tooltip"; more info: . /// Action called whenever opening the submenu and should add items to it. /// Item image. Read here: . /// Disabled state. /// [](xref:caller_info) /// [](xref:caller_info) /// /// The submenu is other object. It inherits many properties of this menu; see property documentation. /// /// /// { /// m["A"] = o => { print.it(o); }; /// m["B"] = o => { print.it(o); }; /// }); /// ]]> /// This code shows dynamically created menu of files in a folder and subfolders. Subfolder files are retrieved when opening the submenu. /// _Dir(m, v as DirectoryInfo)); /// } else { /// m[v.Name]=o=>print.it(v.FullName); /// } /// m.Last.File = v.FullName; /// } /// } /// ]]> /// public PMItem Submenu(string text, Action opening, MTImage image = default, bool disable = false, [CallerLineNumber] int l_ = 0, [CallerFilePath] string f_ = null) => _Add(new PMItem(this, disable) { IsSubmenu = true }, text, image, l_, f_, opening); /// /// Adds menu item that opens a reusable submenu. /// /// Item text. Can include hotkey, tooltip and underlined character, like "Te&xt\t Hotkey\0 Tooltip"; more info: . /// Func called whenever opening the submenu and should return the submenu object. Can return null. /// Item image. Read here: . /// Disabled state. /// [](xref:caller_info) /// [](xref:caller_info) /// /// The caller creates the submenu (creates the object and adds items) and can reuse it many times. Other overload does not allow to create and reuse same object. /// The submenu does not inherit properties of this menu. /// /// /// m2); /// ]]> /// public PMItem Submenu(string text, Func opening, MTImage image = default, bool disable = false, [CallerLineNumber] int l_ = 0, [CallerFilePath] string f_ = null) => _Add(new PMItem(this, disable) { IsSubmenu = true }, text, image, l_, f_, opening); /// /// Adds separator. /// public void Separator() => _Add(new PMItem(this, isDisabled: true) { IsSeparator = true }, null, default, 0, null); /// /// Gets the last added menu item. /// public PMItem Last { get; private set; } /// /// Gets added items, except separators and items in submenus. /// /// /// Allows to set properties of multiple items in single place instead of after each "add item" code line. /// /// Does not get items in submenus. Submenus are separate objects and you can use their Items property. /// public IEnumerable Items { get { _ThreadTrap(); foreach (var v in _a) { if (!v.IsSeparator) yield return v; } } } /// /// Gets added items and separators, except items in submenus. /// public IReadOnlyList ItemsAndSeparators { get { _ThreadTrap(); return _a; } } /// /// Don't use: & character for keyboard shortcut; tab character for hotkey; | character for tooltip (but use \0). /// This property is applied to items added afterwards; submenus inherit it. /// public bool RawText { get; set; } /// /// Adds enum members as checkbox-items (if it's a [Flags] enum) or radio-items. /// /// Object for getting result later. See . /// Initial value. /// Enum members and their text/tooltip. Optional. Text can be: null, "text", "text|tooltip", "|tooltip". [EditorBrowsable(EditorBrowsableState.Never)] //obsolete. Too simple. Added EnumUI examples in cookbook. public EnumUI AddEnum(TEnum init = default, (TEnum value, string text)[] items = null) where TEnum : unmanaged, Enum { return new EnumUI(this, init, items); } #endregion #region show, close /// /// Shows the menu and waits until closed. /// /// /// id of the selected item, or 0 if canceled. /// See also: . /// /// /// Menu position in screen. If null (default), uses mouse position by default. It depends on flags. /// The menu should not overlap this rectangle in screen. /// Owner window. The menu will be automatically closed when destroying its owner window. /// The menu is open or is submenu. public int Show(PMFlags flags = 0, POINT? xy = null, RECT? excludeRect = null, AnyWnd owner = default) { _ThreadTrap(); _OpenTrap("this menu is already open"); if (_sub.parent != null) throw new InvalidOperationException("this is a submenu"); if (_a.Count == 0) return 0; Api.ReleaseCapture(); //winforms still capturing on MouseClick etc, and menu would be like disabled if (!flags.Has(PMFlags.Underline)) if (0 != Api.SystemParametersInfo(Api.SPI_GETKEYBOARDCUES, 0)) flags |= PMFlags.Underline; _Show(flags, xy, excludeRect, owner.Hwnd); int R = 0; WindowsHook hKey = null, hMouse = null; timer timer = null; try { var wFore = wnd.active; bool foreground = wFore.IsOfThisThread; //to close with mouse use timer. Mouse hook may not work because of UAC. int mouseState = _GetMouseState(); timer = new(t => { //close if mouse clicked a non-menu window or if activated another window int ms = _GetMouseState(); bool clicked = ms != mouseState; mouseState = ms; if (clicked) _CloseIfClickedNotMenu(wnd.fromMouse(WXYFlags.Raw)); else if (wnd.active != wFore) Close(); }); timer.Every(30); static int _GetMouseState() => (keys.gui.getKeyState(KKey.MouseLeft) & 0x1) | ((keys.gui.getKeyState(KKey.MouseRight) & 0x1) << 1) | ((keys.gui.getKeyState(KKey.MouseMiddle) & 0x1) << 2) ; //note: use only toggled state. Pressed state may change to "no" when mouse is already in a non-menu window although was in a menu window at the time of the mouse event. //note: in some cases toggled state may not change when clicked. Eg when clicked a taskbar button that activates another window. Then helps if (wnd.active!=wFore) Close();. void _CloseIfClickedNotMenu(wnd w) { //if(!w.Get.Owners(andThisWindow: true).Contains(_w)) Close(); //no, user may want nested root menus, although it is rare if (!_IsMenuWindow(w)) Close(); } bool _IsMenuWindow(wnd w) => w == _w || w.ClassNameIs("Au.popupMenu"); if (!foreground) { //never mind: hooks don't work if the active window has higher UAC IL. Then use timer and mouse/Esc toggle state. hKey = WindowsHook.Keyboard(h => { var k = h.Key; if (KeyboardHook != null && !h.IsUp) { switch (KeyboardHook(this, h)) { case PMKHook.None: return; case PMKHook.Close: _w.Post(Api.WM_CLOSE); return; case PMKHook.ExecuteFocused when FocusedItem != null: _w.Post(Api.WM_USER + 50, _a.IndexOf(FocusedItem)); h.BlockEvent(); return; } } #if true if (!_IsCancelKey(k)) { if (_IsPassKey(k)) return; h.BlockEvent(); } if (!h.IsUp) _w.Post(Api.WM_KEYDOWN, (int)k, 0); //else _w.Post(Api.WM_KEYUP, (int)k, 0xC0000001); #else //unfinished. The idea was to call TranslateMessage, and if then PeekMessage gets wm_char... if(_IsCancelKey(k)) { _w.Post(Api.WM_CLOSE); return; } if (!h.IsUp) { var ok=Api.TranslateMessage(new() { hwnd = _w, message = Api.WM_KEYDOWN, wParam = (int)k }); //print.it(ok); if(Api.PeekMessage(out var v, _w, Api.WM_CHAR, Api.WM_CHAR, Api.PM_NOREMOVE)) print.it("peek", v); } #endif }); //If the active app is showing a menu, it captures the mouse. // If this menu is there, a click goes to that app instead of this menu. // Workaround: mouse hook. We cannot SetCapture in this background thread. if (_IsCapturingMouse()) { hMouse = WindowsHook.Mouse(h => { if (h.IsInjected) return; if (!_IsCapturingMouse()) { h.hook.Unhook(); return; } //tested: mouse move and wheel works without this. if (h.IsButton && h.Button is MButton.Left or MButton.Right or MButton.Middle) { var p = mouse.xy; var w = wnd.fromXY(p, WXYFlags.Raw); if (!_IsMenuWindow(w)) return; h.BlockEvent(); int m = h.Button switch { MButton.Left => Api.WM_LBUTTONDOWN, MButton.Right => Api.WM_RBUTTONDOWN, _ => Api.WM_MBUTTONDOWN }; if (h.IsButtonUp) m++; w.MapScreenToClient(ref p); w.Post(m, 0, Math2.MakeLparam(p)); } }); } static bool _IsCapturingMouse() => miscInfo.getGUIThreadInfo(out var g) && !g.hwndCapture.Is0; } //var pmo = new PrintMsgOptions(Api.WM_TIMER, Api.WM_MOUSEMOVE, Api.WM_NCMOUSEMOVE, Api.WM_PAINT, 0x138a /*SC_WORK_IDLE*/, Api.WM_USER, int.MinValue) { WindowProperties = true }; //print.it("in"); _MessageLoop(); //print.it("out"); void _MessageLoop() { do { for (; ; ) { if (_w.Is0) return; if (!Api.PeekMessage(out var m, default, 0, 0, Api.PM_NOREMOVE)) break; if (m.message == Api.WM_QUIT) return; //let outer loop get the message (tested) bool handled = false; if (m.message is Api.WM_LBUTTONDOWN or Api.WM_RBUTTONDOWN or Api.WM_MBUTTONDOWN or Api.WM_NCLBUTTONDOWN or Api.WM_NCRBUTTONDOWN or Api.WM_NCMBUTTONDOWN) { _CloseIfClickedNotMenu(m.hwnd); if (_w.Is0) return; //let outer loop get the message } else if (m.message is Api.WM_KEYDOWN or Api.WM_SYSKEYDOWN) { handled = _WmKeydown(m); if (!handled && _w.Is0) return; //let outer loop get the message. Used for keys that close the menu but must be passed to the app, eg Alt. If Esc, handled is true. } if (!Api.PeekMessage(out m)) break; if (handled) continue; //WndUtil.PrintMsg(m, pmo); if (m.message == Api.WM_CHAR) { _TopMenu()._WmChar((char)m.wParam); continue; } Api.TranslateMessage(m); Api.DispatchMessage(m); } } while (Api.WaitMessage()); //why this strange loop? // If the click or keydown closed the menu and wants to pass the message to the app, before passing it need to exit the loop, else bad things may happen. // Or could use GetMessage + PostMessage, but it is probably more dirty; need to repost all queued messages, to avoid eg lbuttondown after lbuttonup. } } finally { hKey?.Dispose(); hMouse?.Dispose(); timer?.Stop(); if (!_w.Is0) Api.DestroyWindow(_w); } var b = _result; if (b != null) { b.InvokeAction_(); R = b.Id; } return R; } /// /// Returns true if the menu window is open. /// public bool IsOpen => !_w.Is0; /// /// After closing the menu gets the selected item, or null if canceled. /// public PMItem Result => _result; void _OpenTrap(string error = null) { if (IsOpen) throw new InvalidOperationException(error); } void _ShowSubmenu(PMItem b, bool focusFirst = false, popupMenu m = null) { if (b == _sub.item) return; _sub.child?.Close(); bool contextMenu = m != null; if (!contextMenu) { if (b.clicked == null) return; if (b.clicked is Action menu) { m = b.sourceFile == null ? new() : new popupMenu(null, b.sourceFile, b.sourceLine); base._CopyProps(m); m.CheckDontClose = CheckDontClose; m.RawText = RawText; menu(m); } else if (b.clicked is Func func) { m = func(); } if (m == null || m._a.Count == 0) return; } _sub.child = m; _sub.item = b; m._sub.parent = this; if (focusFirst && m._iHot < 0) m._iHot = 0; if (contextMenu) { m._Show(0, null, null, _w); } else { var r = _ItemRect(b, inScreen: true); r.Inflate(-_size.border, _size.border); m._Show(_flags & ~(PMFlags)0xffffff, new(r.right, r.top), r, _w); } } void _Show(PMFlags flags, POINT? xy, RECT? excludeRect, wnd owner) { if (_a.Count == 0) return; _result = null; _flags = flags; RECT cr = default; bool byCaret = flags.Has(PMFlags.ByCaret); if (byCaret) { if (caretRectFunc is { } crf) { var r1 = crf(); if (r1 is null) byCaret = false; else cr = r1.Value; } else byCaret = miscInfo.getTextCursorRect(out cr, out _); } POINT p = byCaret ? new(cr.left, cr.bottom) : xy ?? mouse.xy; var scrn = screen.of(p); _dpi = scrn.Dpi; var rs = scrn.WorkArea; rs.Inflate(-8, -8); if (byCaret) { if (excludeRect == null) { cr.Inflate(50, 1); excludeRect = cr; flags |= PMFlags.AlignRectBottomTop; } } else { if (flags.Has(PMFlags.WindowCenter) && wnd.active is wnd wa && wa.GetRect(out var rw)) { p = new(rw.CenterX, rw.CenterY); flags |= PMFlags.AlignCenterH | PMFlags.AlignCenterV; } else if (flags.Has(PMFlags.ScreenCenter)) { p = new(rs.CenterX, rs.CenterY); flags |= PMFlags.AlignCenterH | PMFlags.AlignCenterV; } else if (excludeRect == null && !flags.HasAny(PMFlags.AlignCenterH | PMFlags.AlignCenterV)) { excludeRect = new(p.x, p.y, 1, 1); } } if (_addedNewItems) { _addedNewItems = false; _Images(); } _scroll = new(true, i => _a[i].rect.top, i => _a[i].rect.bottom); WS style = WS.POPUP | WS.DLGFRAME; //3-pixel frame WSE estyle = WSE.TOOLWINDOW | WSE.NOACTIVATE | WSE.TOPMOST; SIZE z = _Measure(rs.Width * 19 / 20); bool needScroll = z.height > rs.Height; if (needScroll) { z.height = rs.Height; style |= WS.VSCROLL; } if (byCaret && !flags.HasAny(PMFlags.AlignRight | PMFlags.AlignCenterH)) p.x = Math.Max(p.x - _met.image - _met.check - _met.paddingLeft - _met.textPaddingX - 3, rs.left); RECT r = new(0, 0, z.width, z.height); Dpi.AdjustWindowRectEx(_dpi, ref r, style, estyle); _size.window = r.Size; _size.client = z; _size.border = -r.top; Api.CalculatePopupWindowPosition(p, r.Size, (uint)flags & 0xffffff, excludeRect.GetValueOrDefault(), out r); if (r.bottom > rs.bottom && r.top > rs.top - 4) r.Move(r.left, r.top - 4); //let the bottom edge not touch the bottom edge of the screen _w = WndUtil.CreateWindow(_WndProc, true, "Au.popupMenu", null, style, estyle, r.left, r.top, r.Width, r.Height, owner); _SetScrollbar(needScroll); _w.ShowL(true); _mouse.p = _w.MouseClientXY; } /// /// Closes the menu and its submenus. /// /// If this is a submenu, close the root menu with all submenus. /// /// Can be called from any thread. /// Does nothing if not open. /// public void Close(bool ancestorsToo = false) { if (!IsOpen) return; if (_IsOtherThread) { _w.Post(Api.WM_CLOSE, ancestorsToo ? 1 : 0); } else { var w = _w; if (ancestorsToo) { for (var pm = _sub.parent; pm != null; pm = pm._sub.parent) { w = pm._w; pm._result = _result; } } Api.DestroyWindow(w); } } private protected override void _WmNcdestroy() { //print.it("destroy", _name); _sub.timer?.Stop(); var pa = _sub.parent; if (pa != null) { pa._sub.child = null; pa._sub.item = null; } else { _w.Post(0); } _met?.Dispose(); _met = null; _font?.Dispose(); _font = null; _fontBold?.Dispose(); _fontBold = null; _scroll = null; _sub = default; _size = default; _mouse = default; _iHot = -1; base._WmNcdestroy(); } #endregion nint _WndProc(wnd w, int msg, nint wParam, nint lParam) { //var pmo = new PrintMsgOptions(Api.WM_NCHITTEST, Api.WM_SETCURSOR, Api.WM_MOUSEMOVE, Api.WM_NCMOUSEMOVE, 0x10c1); //if (WndUtil.PrintMsg(out string s, w, msg, wParam, lParam, pmo)) print.it("<>" + s + "<>"); //WndUtil.PrintMsg(w, msg, wParam, lParam); if (_scroll.WndProc(w, msg, wParam, lParam)) return default; switch (msg) { case Api.WM_NCCREATE: _WmNccreate(w); break; case Api.WM_NCDESTROY: _WmNcdestroy(); break; case Api.WM_CLOSE: Close(ancestorsToo: 0 != (wParam & 1)); return default; //case Api.WM_THEMECHANGED: //don't need for a menu window // _z?.Dispose(); // _z = new _Metrics(this); // Api.InvalidateRect(w); // break; case Api.WM_ERASEBKGND: return default; case Api.WM_PAINT: using (var bp = new BufferedPaint(w, true)) _Render(bp.DC, bp.UpdateRect); return default; case Api.WM_MOUSEACTIVATE: return Api.MA_NOACTIVATE; case Api.WM_MOUSEMOVE: _WmMousemove(lParam, fake: false); return default; case Api.WM_MOUSELEAVE: _WmMouseleave(); return default; case >= Api.WM_LBUTTONDOWN and <= Api.WM_MBUTTONUP: _WmMousebutton(msg, lParam); return default; case Api.WM_GETOBJECT: if (_WmGetobject(wParam, lParam, out var r1)) return r1; break; case Api.WM_USER + 50: //posted by acc dodefaultaction or PMKHook.ExecuteFocused if (IsOpen) { int i = (int)wParam; if ((uint)i < _a.Count) _Click(i); } return default; } var R = Api.DefWindowProc(w, msg, wParam, lParam); switch (msg) { case Api.WM_NCPAINT: _WmNcpaint(); break; } return R; } void _SetScrollbar(bool needScroll) { if (needScroll) { _scroll.SetRange(_a.Count); _scroll.PosChanged += (sb, part) => { _sub.child?.Close(); int pos = _scroll.Pos; Api.InvalidateRect(_w); if (part <= -2) { //if mouse wheel, update hot item, submenu, tooltip var p = _w.MouseClientXY; if (_w.ClientRect.Contains(p)) _WmMousemove(Math2.MakeLparam(p), fake: true); } }; _scroll.Visible = true; } else { _scroll.NItems = _a.Count; } } int _HitTest(POINT p, bool failIfDisabled = false) { p.y += _scroll.Offset; for (int i = 0; i < _a.Count; i++) { if (_a[i].rect.Contains(p)) return (failIfDisabled && _a[i].IsDisabled) ? -1 : i; } return -1; } RECT _ItemRect(PMItem k, bool inScreen = false) { var r = k.rect; r.Offset(0, -_scroll.Offset); if (inScreen) _w.MapClientToScreen(ref r); return r; } void _WmMousemove(nint lParam, bool fake) { var p = Math2.NintToPOINT(lParam); //prevent selecting item when mouse position does not change. It would interfere with keyboard navigation. if (!fake && p == _mouse.p) return; _mouse.p = p; int i = _HitTest(p, failIfDisabled: true); if (i != _iHot) { _SetHotItem(i); if (i >= 0) { var b = _a[i]; int submenuDelay = _SubmenuTimer(b.IsSubmenu ? b : null); _SetTooltip(b, _ItemRect(b), lParam, submenuDelay); } else { _HideTooltip(); _SubmenuTimer(); } } if (_iHot >= 0 != _mouse.track) _mouse.track = Api.TrackMouseLeave(_w, _iHot >= 0) && _iHot >= 0; _sub.parent?._SubmenuMouseMove(); } int _SubmenuTimer(PMItem item = null) { if (item == null && _sub.child == null) return 0; _sub.timer ??= new(t => { if (t.Tag is PMItem mi) { if (FocusedItem == mi) _ShowSubmenu(mi); } else if (_sub.child != null && !_sub.child._w.Rect.Contains(mouse.xy)) { _sub.child.Close(); } }); int R = Api.SystemParametersInfo(Api.SPI_GETMENUSHOWDELAY, 400); _sub.timer.Tag = item; _sub.timer.After(R); return R; } void _SubmenuMouseMove() { if (FocusedItem != _sub.item) { _SetHotItem(_a.IndexOf(_sub.item)); _sub.timer.Stop(); } } void _WmMouseleave() { _mouse.track = false; if (_iHot < 0) return; if (_sub.child?._w.Rect.Contains(mouse.xy) ?? false) return; _SetHotItem(-1); _SubmenuTimer(); } void _WmMousebutton(int msg, nint lParam) { switch (msg) { case Api.WM_LBUTTONDOWN: _mouse.left = true; return; case Api.WM_LBUTTONUP: if (!_mouse.left) return; _mouse.left = false; break; case Api.WM_RBUTTONDOWN: _mouse.right = true; return; case Api.WM_RBUTTONUP: if (!_mouse.right) return; _mouse.right = false; break; case Api.WM_MBUTTONDOWN: _mouse.middle = true; return; case Api.WM_MBUTTONUP: if (_mouse.middle) Close(ancestorsToo: true); return; default: return; } var p = Math2.NintToPOINT(lParam); int i = _HitTest(p); if (i < 0) return; if (msg == Api.WM_LBUTTONUP) _Click(i); else _ContextMenu(_a[i]); } void _Click(int i, bool keyboard = false) { var b = _a[i]; if (b.IsDisabled) return; if (b.checkType > 0) { if (b.checkType == 1) { b.IsChecked ^= true; } else if (!b.IsChecked) { for (int j = i; --j >= 0 && _Uncheck(j);) { } for (int j = i; ++j < _a.Count && _Uncheck(j);) { } b.IsChecked = true; bool _Uncheck(int j) { var v = _a[j]; if (v.checkType != 2) return false; v.IsChecked = false; return true; } } if (b.checkDontClose) { b.InvokeAction_(); return; } } if (b.IsSubmenu) { if (keyboard) _SetHotItem(i, ensureVisible: true); _ShowSubmenu(b, focusFirst: keyboard); } else { _result = b; Close(ancestorsToo: true); } } /// /// Don't close menu when clicked a checkbox or radio item. /// This property is applied to items added afterwards; submenus inherit it. /// public bool CheckDontClose { get; set; } void _ContextMenu(PMItem b) { if (b.IsSeparator) return; var (canEdit, canGo, goText) = MTItem.CanEditOrGoToFile_(b.sourceFile, b); if (canEdit || canGo) { var m = new popupMenu(); if (canEdit) m["Edit menu item"] = _ => ScriptEditor.Open(b.sourceFile, b.sourceLine); if (canGo) m[goText] = _ => b.GoToFile_(); _ShowSubmenu(b, m: m); } } popupMenu _TopMenu() { var m = this; while (m._sub.child != null) m = m._sub.child; return m; } bool _TopHot(out (popupMenu m, int i, PMItem b) r) { var m = this; while (m._sub.child != null) m = m._sub.child; int i = m._iHot; r = (m, i, i < 0 ? null : m._a[i]); return i >= 0; } /// /// Gets or sets the focused menu item. /// /// /// The focused item visually shows the menu item that would be executed if clicked or pressed Enter, Tab or Space key. It changes when the user moves the mouse or presses navigation keys (arrows, End, Home, PageDown, PageUp). /// This property can be set before showing the menu or when it is open. /// public PMItem FocusedItem { get => _iHot >= 0 ? _a[_iHot] : null; set { _ThreadTrap(); int i = -1; if (value != null) { i = _a.IndexOf(value); if (i < 0) throw new ArgumentException(); } if (_w.Is0) { _iHot = i; } else { _SetHotItem(i, ensureVisible: true); } } } void _SetHotItem(int i, bool ensureVisible = false) { if (i != _iHot) { if (_iHot >= 0) _Invalidate(_iHot); if ((_iHot = i) >= 0) _Invalidate(_iHot); } if (ensureVisible && i >= 0) { int pos = _scroll.Pos, y = _scroll.Offset, hei = _size.client.height; var r = _a[i].rect; if (r.top < y) { while (pos > 0 && r.top < y) y -= _a[--pos].rect.Height; } else if (r.bottom > y + hei) { while (pos < _scroll.Max && r.bottom > y + hei) y += _a[++pos].rect.Height; } else return; _scroll.Pos = pos; } } bool _WmKeydown(in MSG msg) { //called for root menu KKey k = (KKey)(int)msg.wParam; if (_IsCancelKey(k)) { Close(); return false; } else if (_IsOkKey(k)) { if (_TopHot(out var v)) v.m._Click(v.i, keyboard: true); } else if (k == KKey.Escape) { _TopMenu().Close(); } else if (k == KKey.Left) { var m = _TopMenu(); if (m != this) m.Close(); } else if (k == KKey.Right) { if (_TopHot(out var v) && v.b.IsSubmenu && !v.b.IsDisabled) v.m._ShowSubmenu(v.b, focusFirst: true); } else if (k is KKey.Down or KKey.Up or KKey.PageDown or KKey.PageUp or KKey.End or KKey.Home) { _TopMenu()._KeyNavigate(k); } else if (_IsPassKey(k)) { return false; } else { //eg a char key. Translate to get wm_char (we'll eat it), but eat this wm_keydown. Api.TranslateMessage(msg); } return true; } void _KeyNavigate(KKey k) { //called for top menu int i = _scroll.KeyNavigate(_iHot, k); if (i == _iHot) return; while ((uint)i < _a.Count && _a[i].IsSeparator) i += k is KKey.Home or KKey.Down or KKey.PageDown ? 1 : -1; _SetHotItem(Math.Clamp(i, 0, _a.Count - 1), ensureVisible: true); } void _WmChar(char c) { //called for top menu if (c <= ' ') return; char cl = char.ToLowerInvariant(c), cu = char.ToUpperInvariant(c); int iUnderlined = -1; List aUnderlined = null; for (int i = 0; i < _a.Count; i++) { var v = _a[i]; if (v.rawText || v.IsDisabled) continue; string s = v.Text; int j = StringUtil.FindUnderlineChar(s); if (j >= 0 && (s[j] == cu || s[j] == cl)) { if (iUnderlined < 0) iUnderlined = i; else (aUnderlined ??= new() { iUnderlined }).Add(i); } } if (aUnderlined != null) { int fi = 0; if (_iHot >= 0) { for (int i = 0; i < aUnderlined.Count; i++) if (aUnderlined[i] > _iHot) { fi = i; break; } } FocusedItem = _a[aUnderlined[fi]]; } else if (iUnderlined >= 0) { _Click(iUnderlined, keyboard: true); } } static bool _IsOkKey(KKey k) => k is KKey.Enter or KKey.Tab or KKey.Space; static bool _IsCancelKey(KKey k) => k is KKey.Alt or KKey.Win or KKey.RWin or KKey.F10 or KKey.Apps or KKey.Back; static bool _IsPassKey(KKey k) => k is KKey.Ctrl or KKey.Shift or KKey.CapsLock or KKey.NumLock or KKey.ScrollLock or KKey.PrintScreen or KKey.Pause or KKey.Insert or (>= KKey.F1 and <= KKey.F24) || keys.isMod(KMod.Ctrl | KMod.Alt | KMod.Win); /// /// Creates and shows a simple popup menu. Without images, actions, submenus. Returns item id or 0. /// /// id of the selected item when closed, or 0 if canceled. /// /// Menu items, like "One|Two|Three" or new("One", "Two", "Three") or string array or List. /// Item id can be optionally specified like "1 One|2 Two|3 Three", unless rawText true. If missing, uses id of previous non-separator item + 1. Example: "One|Two|100 Three Four" (1|2|100|101). /// For separators use null or empty strings: "One|Two||Three|Four". /// /// /// Menu position in screen. If null (default), uses mouse position by default. It depends on flags. /// The menu should not overlap this rectangle in screen. /// Owner window. The menu will be automatically closed when destroying its owner window. /// Don't parse id from text. /// /// Adds menu items and calls . Returns when the menu closes. All parameters except items are the same as in . /// /// public static int showSimple(Strings items, PMFlags flags = 0, POINT? xy = null, RECT? excludeRect = null, AnyWnd owner = default, bool rawText = false) { var a = items.ToArray(); var m = new popupMenu(); foreach (var v in a) { var s = v; if (s.NE()) { m.Separator(); } else { if (!rawText && s.ToInt(out int id, 0, out int end)) { if (s.Eq(end, ' ')) end++; s = s[end..]; m.Add(id, s); } else { m.Add(s); } } } return m.Show(flags, xy, excludeRect, owner); } /// /// Gets or sets callback function that decides how to respond to pressed keys (default, close, ignore, block). /// /// /// The function is called on each key down event while the menu is open. Only if current thread is not in the foreground. /// To block a key, call . /// The function must be as fast as possible. /// public Func KeyboardHook { get; set; } /// /// Sets a user-defined "get caret rectangle" function. /// /// Default: null; calls . /// /// The callback function is called by when flags includes . Also used by . /// Let it return a rectangle in screen, or null if failed. For example, it can call ; if it fails, use an alternative way to get caret rectangle or preferred menu location. /// public static Func caretRectFunc { get; set; } } ================================================ FILE: Au/GUI/toolbar/tb acc.cs ================================================ using IAccessible = Au.Types.Api.IAccessible; using VarInt = Au.Types.Api.VarInt; using NAVDIR = Au.Types.Api.NAVDIR; namespace Au; [ComVisible(true)] partial class toolbar : IAccessible { IAccessible IAccessible.get_accParent() => _StdAO.get_accParent(); int IAccessible.get_accChildCount() => _a.Count; int IAccessible.get_accChild(VarInt varChild, out object ppdispChild) { ppdispChild = null; return 1; } string IAccessible.get_accName(VarInt varChild) => _B(varChild, out var b) ? (b.Text ?? b.Tooltip) : _name; string IAccessible.get_accValue(VarInt varChild) => null; string IAccessible.get_accDescription(VarInt varChild) => _B(varChild, out _) ? null : "Floating toolbar"; VarInt IAccessible.get_accRole(VarInt varChild) { Debug_.PrintIf(Api.GetCurrentThreadId() != _w.ThreadId, "thread"); var r = !_B(varChild, out var b) ? ERole.TOOLBAR : b.ItemType switch { TBItemType.Separator => ERole.SEPARATOR, TBItemType.Group => ERole.GROUPING, TBItemType.Menu => ERole.BUTTONMENU, _ => ERole.BUTTON }; return (int)r - 1; } VarInt IAccessible.get_accState(VarInt varChild) { EState r = 0; if (!_w.IsEnabled()) r |= EState.DISABLED; if (!_B(varChild, out var b)) { if (!_w.IsVisible) r |= EState.INVISIBLE; } else { if (b.IsSeparatorOrGroup_) r |= EState.DISABLED; if (b.IsMenu_) r |= EState.HASPOPUP; //TODO3: if offscreen, r |= EState.INVISIBLE | EState.OFFSCREEN; //no: EState.HOTTRACKED; } return (int)r - 1; } string IAccessible.get_accHelp(VarInt varChild) => _B(varChild, out var b) ? _GetFullTooltip(b) : null; int IAccessible.get_accHelpTopic(out string pszHelpFile, VarInt varChild) => throw new NotImplementedException(); string IAccessible.get_accKeyboardShortcut(VarInt varChild) => null; object IAccessible.get_accFocus() => null; object IAccessible.get_accSelection() => null; string IAccessible.get_accDefaultAction(VarInt varChild) => !_B(varChild, out var b) || b.IsSeparatorOrGroup_ ? null : b.IsMenu_ ? "Open" : "Execute"; void IAccessible.accSelect(ESelect flagsSelect, VarInt varChild) => throw new NotImplementedException(); void IAccessible.accLocation(out int pxLeft, out int pyTop, out int pcxWidth, out int pcyHeight, VarInt varChild) { if (!_B(varChild, out var b)) { _StdAO.accLocation(out pxLeft, out pyTop, out pcxWidth, out pcyHeight, varChild); } else { var r = b.rect; _w.MapClientToScreen(ref r); pxLeft = r.left; pyTop = r.top; pcxWidth = r.Width; pcyHeight = r.Height; } } object IAccessible.accNavigate(NAVDIR navDir, VarInt varStart) { int i = varStart; var a = _a; if (navDir == NAVDIR.FIRSTCHILD || navDir == NAVDIR.LASTCHILD) { if (i == -1) return navDir == NAVDIR.FIRSTCHILD ? 1 : a.Count; } else { if (i == -1) return _StdAO.accNavigate(navDir, varStart); switch (navDir) { case NAVDIR.PREVIOUS: if (i > 0) return i; break; case NAVDIR.NEXT: if (++i < a.Count) return i + 1; break; } } return null; } VarInt IAccessible.accHitTest(int xLeft, int yTop) { POINT p = new(xLeft, yTop); _w.MapScreenToClient(ref p); if (!_w.ClientRect.Contains(p)) return _StdAO.accHitTest(xLeft, yTop); return _HitTest(p); } void IAccessible.accDoDefaultAction(VarInt varChild) { if (!_B(varChild, out var b) || b.IsSeparatorOrGroup_) return; _w.Post(Api.WM_USER + 50, (int)varChild); } void IAccessible.put_accName(VarInt varChild, string szName) { } void IAccessible.put_accValue(VarInt varChild, string szValue) { } bool _B(VarInt varChild, out TBItem b) { int i = varChild; if (i == -1) { b = null; return false; } b = _a[i]; return true; } } ================================================ FILE: Au/GUI/toolbar/tb dialog.cs ================================================ //TODO2: sometimes the first time shows the window with several s delay. Can't repro, even after reboot. Possibly more likely after hibernation. The slow code is `new Form();` (even without setting properties). using System.Windows.Forms; namespace Au; public partial class toolbar { [ThreadStatic] static Form s_listWindow; /// /// Creates a window with a list of toolbars of this thread. Can be used to find lost toolbars. /// /// Show the window now, non-modal. If a window shown by this function already exists in this thread - activate it. public static Form toolbarsDialog(bool show = true) { if (show && s_listWindow != null) { s_listWindow.Hwnd().ActivateL(true); return s_listWindow; } var f = new Form { Text = "Active toolbars", Size = new(330, 330), AutoScaleMode = AutoScaleMode.Dpi, StartPosition = FormStartPosition.CenterScreen, Icon = icon.ofThisApp()?.ToGdipIcon() }; f.Load += (_, _) => { f.Hwnd().ActivateL(); }; var lv = new ListView { Dock = DockStyle.Fill, View = View.List, BorderStyle = BorderStyle.None, MultiSelect = false, ContextMenuStrip = new() }; f.Controls.Add(lv); var osdr = new osdRect { Color = 0xff0000, Thickness = 12 }; osdText osdt = null; ListViewItem _osdItem = null; var atb = _Manager._atb; (toolbar tb, bool sat)[] patb = atb.Select(o => (o, o.Satellite?.IsOpen ?? false)).ToArray(); var timer1 = timer.every(250, _ => { bool changed = atb.Count != patb.Length; if (!changed) { for (int i = 0; i < atb.Count; i++) { if (atb[i] != patb[i].tb || (atb[i].Satellite?.IsOpen ?? false) != patb[i].sat) { changed = true; break; } } } if (changed) { _HideRect(); patb = atb.Select(o => (o, o.Satellite?.IsOpen ?? false)).ToArray(); _FillList(); } }); f.FormClosed += (_, _) => { if (show) s_listWindow = null; osdr.Dispose(); timer1.Stop(); }; _FillList(); void _FillList() { lv.Items.Clear(); foreach (var tb in _Manager._atb) { _Add(tb); if (tb.Satellite is { } sat && sat.IsOpen) _Add(sat); } void _Add(toolbar tb) { lv.Items.Add(tb.ToString()).Tag = tb; } } lv.MouseMove += (_, _) => { //note: MouseHover not always works var p = mouse.xy; lv.Hwnd().MapScreenToClient(ref p); var v = lv.HitTest(p).Item; if (v == _osdItem) return; if (v != null) { var tb = v.Tag as toolbar; if (tb.IsOpen) { var w = tb._w; var r = w.Rect; if (screen.isInAnyScreen(r)) { r.Inflate(10, 10); osdr.Rect = r; osdr.Show(); } else { osdt = osdText.showText($"The toolbar is offscreen. Right-click to move.\nRectangle: {r}", xy: PopupXY.Mouse); } v.Selected = true; v.Focused = true; _osdItem = v; } else { _HideRect(); _FillList(); } } else { _HideRect(); } }; lv.MouseLeave += (_, _) => _HideRect(); void _HideRect() { if (_osdItem != null) { _osdItem = null; osdr.Hide(); osdt?.Dispose(); osdt = null; } } lv.ItemActivate += (_, _) => { _Edit(lv.FocusedItem.Tag as toolbar); }; void _Edit(toolbar tb) { ScriptEditor.Open(tb._sourceFile, tb._sourceLine); timer.after(100, _ => f.Hwnd().ZorderTop()); } lv.ContextMenuStrip.Opening += (_, e) => { e.Cancel = true; if (lv.SelectedItems.Count == 0) return; _HideRect(); var tb = lv.FocusedItem.Tag as toolbar; var w = f.Hwnd(); var m = new popupMenu(); m["Edit\tD-click"] = o => _Edit(tb); m["Move here"] = o => { if (!tb.IsOpen) return; var w = tb._w; if (!w.IsVisible && !dialog.showOkCancel("Hidden", "Move this hidden toolbar?", owner: w)) return; w.MoveL_(mouse.xy); if (!w.ZorderIsAbove(w)) w.ZorderAbove(w); }; m.Show(owner: w); }; if (show) f.Show(); return s_listWindow = f; } } ================================================ FILE: Au/GUI/toolbar/tb man.cs ================================================ namespace Au; public partial class toolbar { class _OwnerWindow { public readonly List a = []; public readonly wnd w; public bool visible; bool _updatedOnce; RECT _rect, _clientRect; SIZE _prevSize, _prevClientSize; //public readonly int thread; public _OwnerWindow(wnd w) { this.w = w; //thread = w.ThreadId; } public void AddTB(toolbar tb) { a.Add(tb); tb._ow = this; } public (bool visible, bool dead) IsVisible() { lastError.clear(); if (!w.IsVisible) return (false, lastError.code != 0); return (!w.IsMinimized && !w.IsCloaked, false); //speed: IsCloaked now on Win10 quite fast, faster than GetRect } public bool UpdateRect(out bool changed) { changed = false; //if a toolbar has MaximizedWindowTopPlus, calculate how many physical pixels to add to the top of the window rect or client rect int? mtpw = null, mtpc = null; foreach (var tb in a) { if (tb._oc != null || tb._os != null) continue; if (tb.MaximizedWindowTopPlus != 0 && w.IsMaximized) { ref int? mtp = ref tb._followClientArea ? ref mtpc : ref mtpw; if (mtp != null) print.warning("When multiple toolbars attached to the same window, set MaximizedWindowTopPlus once (for any toolbar), not for all toolbars."); else mtp = tb._Scale(tb.MaximizedWindowTopPlus, true); } } int have = 0; foreach (var tb in a) { if (tb._oc != null || tb._os != null) continue; if (tb._followClientArea) { if (0 != (have & 2)) continue; if (!w.GetClientRect(out var r, inScreen: true)) return false; r.top += mtpc.GetValueOrDefault(); if (r != _clientRect) { _prevClientSize = (_clientRect.Width, _clientRect.Height); _clientRect = r; changed = true; } have |= 2; } else { if (0 != (have & 1)) continue; if (!w.GetRect(out var r)) return false; r.top += mtpw.GetValueOrDefault(); if (r != _rect) { _prevSize = (_rect.Width, _rect.Height); _rect = r; changed = true; } have |= 1; } if (have == 3) break; } if (!_updatedOnce) _updatedOnce = changed = true; return true; } public (RECT r, SIZE size) GetCachedRectAndPrevSize(toolbar tb) => (tb._followClientArea ? _clientRect : _rect, tb._followClientArea ? _prevClientSize : _prevSize); } class _OwnerControl { public readonly wnd c; public readonly ITBOwnerObject oo; public RECT cachedRect; public SIZE prevSize; bool _updatedOnce; public _OwnerControl(wnd control, ITBOwnerObject ioo) { c = control; oo = ioo; } public (bool visible, bool dead) IsVisible(bool parentVisible = true) { if (!c.Is0) { lastError.clear(); if (!c.IsVisible) return (false, lastError.code != 0); if (!parentVisible || c.IsMinimized) return default; //never mind: ancestors controls may be minimized } if (oo != null) { if (!oo.IsAlive) return (false, true); if (!oo.IsVisible) return default; } return (true, false); } public bool UpdateRect(out bool changed) { bool ok = oo != null ? oo.GetRect(out RECT r) : c.GetRect(out r); if (changed = ok && r != cachedRect) { prevSize = (cachedRect.Width, cachedRect.Height); cachedRect = r; } if (!_updatedOnce) _updatedOnce = changed = true; return ok; } } class _OwnerScreen { public _OwnerScreen(toolbar tb, screen scrn) { _tb = tb; _scrn = (_isAuto = scrn.IsEmpty) ? screen.of(_tb._sett.screenx, _tb._sett.screeny) : scrn.Now; UpdateRect(out _); } toolbar _tb; screen _scrn; bool _isAuto; public RECT cachedRect; public SIZE prevSize; #if DEBUG public screen Screen { get { Debug_.PrintIf(!_scrn.IsAlive, "screen not alive"); return _scrn; } } #else public screen Screen => _scrn; #endif //public bool IsAuto => _isAuto; public bool UpdateRect(out bool changed) { RECT r = _scrn.Rect; if (changed = r != cachedRect && !r.Is0) { prevSize = (cachedRect.Width, cachedRect.Height); cachedRect = r; } return true; } //called from _WmWindowPosChanged public void UpdateIfAutoScreen() { if (!_isAuto) return; var r = screen.of(_tb._w).Rect; int x = 0, y = 0; if (r.left != 0 || r.top != 0) { x = r.CenterX; y = r.CenterY; } if (x != _tb._sett.screenx || y != _tb._sett.screeny) { _scrn = screen.of(_tb._sett.screenx = x, _tb._sett.screeny = y); UpdateRect(out _); } } //Called on WM_DISPLAYCHANGE. If screen detached, sets _scrn = 0. When reattached, sets _scrn = new screen handle. public bool IsScreenInvalid() { if (!_scrn.IsAlive) { //Debug_.Print($"CloseIfScreenInvalid, {_tb.Name}, {screen.of(_tb._w, SODefault.Zero)}"); _scrn = screen.of(new POINT(cachedRect.CenterX, cachedRect.CenterY), SODefault.Zero); return _scrn.Handle == default; } return false; } } _OwnerWindow _ow; //not null if owned _OwnerControl _oc; //not null if owned by a control or other object (ITBOwnerObject) _OwnerScreen _os; //not null if not owned or if anchor has flag Screen bool _followClientArea; [ThreadStatic] static _TBManager t_man; static _TBManager _Manager => t_man ??= new(); class _TBManager { internal readonly List _atb = []; readonly List<_OwnerWindow> _aow = []; timer _timer; int _timerPeriod; WinEventHook _hook; int _tempHook; bool _inHook; public void Add(toolbar tb, wnd w, wnd c, ITBOwnerObject ioo) { bool isOwned = !w.Is0; if (isOwned) { if (!_FindOW(w, out var ow)) _aow.Add(ow = new _OwnerWindow(w)); ow.AddTB(tb); if (!c.Is0 || ioo != null) tb._oc = new _OwnerControl(c, ioo); } _atb.Add(tb); if (_hook == null) { _hook = new WinEventHook([ 0, EEvent.OBJECT_REORDER, EEvent.OBJECT_CLOAKED, EEvent.OBJECT_UNCLOAKED, EEvent.SYSTEM_MOVESIZESTART, EEvent.SYSTEM_MOVESIZEEND, EEvent.SYSTEM_MINIMIZESTART, EEvent.SYSTEM_MINIMIZEEND, ], _Hook, flags: EHookFlags.SKIPOWNTHREAD); _timer = new timer(_Timer); } tb._hide = TBHide.Owner; if (isOwned) { _SetTimer(250); } else { if (!_timer.IsRunning) _SetTimer(250); tb._FollowRect(); //tb._Zorder(); tb._SetVisible(true, TBHide.Owner); } } public void Remove(toolbar tb) { _atb.Remove(tb); var ow = tb._ow; if (ow != null) { ow.a.Remove(tb); if (ow.a.Count == 0) _aow.Remove(ow); } } void _SetTimer(int period) { _timer.Every(_timerPeriod = period); } void _Timer(timer t) { if (_timerPeriod != 250) _SetTimer(250); //remove closed toolbars and their owners if need. Now don't need because toolbars call Remove when closing. //for(int i = _atb.Count; --i >= 0;) { // var tb = _atb[i]; // if(tb._closed) Remove(tb); //} //move/close/hide/show owned toolbars together with their owners for (int i = _aow.Count; --i >= 0;) { var ow = _aow[i]; if (!_FollowOwner(ow)) { for (int j = ow.a.Count; --j >= 0;) { var tb = ow.a[j]; tb.Close(); bool rem1 = _atb.Remove(tb); Debug_.PrintIf(rem1, ""); } bool rem2 = _aow.Remove(ow); Debug_.PrintIf(rem2, ""); //actually don't need these two Remove, because tb.Close calls Remove. Just don't use RemoveAt and foreach. } } //occasionally may fail to zorder a toolbar. Retry several times. for (int i = _atb.Count; --i >= 0;) { var v = _atb[i]; if (v._zorderRetry > 0) v._ZorderOwned(); } _ZorderTimer(); _ManageFullScreen(); } //long _reorderTime; void _Hook(HookData.WinEvent d) { //print.it(d.event_, d.idObject, d.idChild, d.thread, d.w); if (d.w.Is0 || d.idObject != (d.event_ == EEvent.OBJECT_REORDER ? EObjid.CLIENT : EObjid.WINDOW) || d.idChild != 0) return; switch (d.event_) { case EEvent.OBJECT_REORDER when d.w == wnd.getwnd.root: //the hook does not give the window, only its thread id _Zorder(); break; case EEvent.SYSTEM_MOVESIZESTART when _tempHook == 0: if (_FindOW(d.w, out _)) _tempHook = _hook.Add(EEvent.OBJECT_LOCATIONCHANGE, flags: EHookFlags.SKIPOWNTHREAD); break; case EEvent.SYSTEM_MOVESIZEEND when _tempHook != 0: _hook.Remove(_tempHook); _tempHook = 0; break; case EEvent.OBJECT_LOCATIONCHANGE: case EEvent.OBJECT_CLOAKED: case EEvent.OBJECT_UNCLOAKED: case EEvent.SYSTEM_MINIMIZESTART: //Debug_.PrintIf(_inHook, "_inHook"); //it's ok if (!_inHook && _FindOW(d.w, out _OwnerWindow ow)) { //prevent reenter. // The ITBOwnerObject may retrieve sent messages, eg when getting acc rect. // It's ok if hook missed. We'll call it on timer or next OBJECT_LOCATIONCHANGE. _inHook = true; try { _FollowOwner(ow); } finally { _inHook = false; } } break; case EEvent.SYSTEM_MINIMIZEEND: if (_FindOW(d.w, out _)) _SetTimer(150); break; } //SYSTEM_MOVESIZESTART and SYSTEM_MOVESIZEEND temporarily add/remove OBJECT_LOCATIONCHANGE to move toolbars with the owner window. // Cannot make OBJECT_LOCATIONCHANGE always active, because it is called frequently, on each cursor position change etc. // There are no other not-in-process hooks to detect moved windows. For CBT hook need 2 processes - 64bit and 32bit. //OBJECT_REORDER keeps toolbars above their owner windows in the Z order. // Easier would be to make the owner natively owner. But then problems: // 1. If this process is admin, the owner's process cannot receive drag&drop from other non-admin processes. Don't know why, probably it is a Windows bug. // 2. Fails if owner's process is a Store app. Also probaby if higher UAC IL. // 3. In some cases possible various anomalies, for example wrong Z order of windows after closing the owner window. // 4. All unknown and future things like those. // In QM some of these problems were solved by adding a child window to the owner window and making it the native owner of the toolbar. But then other problems, eg DPI-scaling. //PROBLEM: OBJECT_REORDER makes creating windows slower. // For example, combobox controls send OBJECT_REORDER when adding items. Two for each item that would be visible in the drop-down list. // Tested: standard dialog box with 12 comboboxes, each with 30 such items. We receive ~720 OBJECT_REORDER. // If there are 4 processes with 1 OBJECT_REORDER hook, dialog startup time increases 50%, from 360 to 540 ms. //Other used hooks aren't called frequently. Except OBJECT_LOCATIONCHANGE, but it is temporary. } bool _FindOW(wnd owner, out _OwnerWindow ow) { foreach (var v in _aow) if (v.w == owner) { ow = v; return true; } ow = null; return false; } bool _FollowOwner(_OwnerWindow ow) { var (visibleW, dead) = ow.IsVisible(); if (dead) return false; bool changedRectW = false; if (visibleW) visibleW = ow.UpdateRect(out changedRectW); ow.visible = visibleW; for (int i = ow.a.Count; --i >= 0;) { bool visible, changedRect; var tb = ow.a[i]; var oc = tb._oc; if (oc == null) { visible = visibleW; changedRect = changedRectW; } else { (visible, dead) = oc.IsVisible(visibleW); if (dead) { tb.Close(); ow.a.RemoveAt(i); continue; } if (visible) visible = oc.UpdateRect(out changedRect); else changedRect = false; } bool changedVisible = visible == tb._hide.Has(TBHide.Owner); if (visible && (changedRect || changedVisible)) { // || changedVisible is for new toolbars, but it's ok to call for old too tb._FollowRect(true); } if (changedVisible) { tb._SetVisible(visible, TBHide.Owner); if (visible) tb._ZorderOwned(); } } return true; } void _ManageFullScreen(toolbar tb = null) { if (tb?.MiscFlags.Has(TBFlags.HideWhenFullScreen) ?? _atb.Any(o => o.MiscFlags.Has(TBFlags.HideWhenFullScreen))) { var w = wnd.active; bool isFS = w.IsFullScreen_(out var scrn); if (tb != null) tb._ManageFullScreen(isFS, w, scrn); else foreach (var v in _atb) if (v.MiscFlags.Has(TBFlags.HideWhenFullScreen)) v._ManageFullScreen(isFS, w, scrn); } } //EEvent.OBJECT_REORDER void _Zorder() { for (int i = _atb.Count; --i >= 0;) { var tb = _atb[i]; if (tb.IsOwned) tb._ZorderOwned(); else if (_zorderDelay < 1) _zorderDelay = 2; } #if false //This version of zordering owned toolbars is faster but unreliable. // 1. For console windows getwindowthreadprocessid gives wrong thread id. Hook receives the correct id. // 2. When clicked client area of a Store app, hook receives thread id of the child control. It is different than that of the main host window. // 3. All unknown and future things like those. foreach(var v in _aow) { if(v.thread != d.idThread) continue; foreach(var tb in v.a) tb._ZorderOwned(); } #endif } int _zorderDelay; void _ZorderTimer() { //250-500 ms after EEvent.OBJECT_REORDER if (_zorderDelay < 1 || --_zorderDelay > 0) return; //Ensure the toolbar is on top of the primary taskbar. Or ontop of the active window if the primary taskbar is behind it. // Not on top of all topmost windows. Would cover tooltips etc, fight with sibling toolbars, etc. var taskbar = s_taskbar.FindFast(null, "Shell_TrayWnd", false); //of the primary screen var active = wnd.active; var w = !active.Is0 && active != taskbar && (taskbar.Is0 || !taskbar.ZorderIsAbove(active) && !active.IsOfThisProcess) ? active : taskbar; if (w.Is0) return; for (int i = _atb.Count; --i >= 0;) { var v = _atb[i]; if (v.IsOwned) continue; var tb = v.Hwnd; if (!tb.ZorderIsAbove(w) && w.IsVisible) { if (w.IsTopmost) { tb.ZorderL_(w, before: true); } else if (!tb.IsTopmost) { tb.ZorderL_(SpecHWND.TOPMOST); } else { //Windows 11 bug: sometimes, when activated a non-topmost window, it becomes on top of topmost windows, eg of taskbars and unowned toolbars. // Afterwards activating other normal windows makes them on top of topmost windows too. // Impossible to reproduce, it happens randomly, once in several days or weeks. Debug_.Print($"toolbar behind the active non-topmost window: {w}"); //workaround 1. Sometimes works. Fails if w is "Caret Listener Shim Window"; then 2 fails too. tb.ZorderL_(SpecHWND.NOTOPMOST); tb.ZorderL_(SpecHWND.TOPMOST); if (!tb.ZorderIsAbove(w)) { Debug_.Print("Workaround 1 failed."); //workaround 2. Not much tested here, but `w1.ZorderL_(w2); w2.ZorderL_(w1);` works elsewhere. tb.ZorderL_(w, before: true); //may not work, although returns true. ZorderL_ gets previous window, and it may be HWND_TOP (0). SWP(HWND_TOP) does nothing if tb is behind the active non-topmost window (w). if (!tb.ZorderIsAbove(w)) { //workaround tb.ZorderL_(w); w.ZorderL_(tb); Debug_.PrintIf(!tb.ZorderIsAbove(w), $"Workaround 2 failed."); } tb.ZorderL_(SpecHWND.TOPMOST); } } } } } static wnd.Cached_ s_taskbar; } void _ZorderOwned() { Debug.Assert(IsOwned); if (IsOwned && _ow.visible) { wnd wt = _w, wo = _ow.w; //Some windows, eg UiPath, have an owned "shadow frame" window that may cover toolbar parts that aren't entirely inside the owner window's rect. // Toolbars should detect such windows and zorder above them. Can't just blindly zorder above the topmost owned. // But detecting can be difficult/slow/unreliable. Need to get all owned windows or all thread windows, etc. // Never mind. Let users don't move toolbars to such places where they may be covered. //if (_zorderedOnce && wo.IsActive) { // var w2 = wo.Get.EnabledOwned(); //may be not that window, eg a tooltip // //print.it("eo", w2); // if (!w2.Is0 && w2.Rect.Contains(wo.Rect)) wo = w2; //} //print.it(wo); if (!_zordered || !wt.ZorderIsAbove(wo)) { _zordered = wt.ZorderAbove(wo) || !wo.IsAlive; if (!_zordered) { var ec = lastError.code; if (wt.ZorderIsAbove(wo)) { _zordered = true; } else if ( #if !DEBUG _zorderRetry == 1 //the last retry #else _zorderRetry > 0 //any retry. It's OK if sometimes fails first time, brobably then it's a bad time to zorder. #endif ) { var es = ec == Api.ERROR_ACCESS_DENIED && wo.UacAccessDenied ? "This process should run as admin, or owner's process not as admin." : lastError.messageFor(ec); print.warning($"Failed to Z-order toolbar '{_name}' above owner window. {es}"); } } } else if (wt.IsTopmost && !wo.IsTopmost) { wt.ZorderAbove(wo); } if (_zordered) _zorderRetry = 0; else if (_zorderRetry == 0) _zorderRetry = 5; else _zorderRetry--; //never mind: when clicked owner's title bar, we receive 2 hook events and need to ZorderAbove 2 times. Speed is OK, but flickers more often. // When we ZorderAbove on mouse down, Windows also zorders the window on mouse up, and then we receive second event. // Possible workarounds: // 1. Temporarily make wt nativaly owned by _ow.w. Restore after 500 ms. But fails with higher UAC IL windows and appstore windows. // 2. Temporarily make wt topmost. Restore after 500 ms. But Windows makes it difficult and possibly unreliable. } } bool _zordered; byte _zorderRetry; void _ManageFullScreen(bool isFS, wnd wFore, screen scrn) { if (_inMoveSize) return; bool hide; if (!isFS) hide = false; else if (IsOwned) hide = OwnerWindow == wFore; else hide = screen.of(_w, SODefault.Zero) == scrn; _SetVisible(!hide, TBHide.FullScreen); } void _FollowRect(bool onFollowOwner = false) { if (_inMoveSize) return; if (onFollowOwner && Anchor.OfScreen() && _followedOnce) return; if (!onFollowOwner && _hide.Has(TBHide.Owner) && IsOwned && OwnerWindow.IsMinimized) return; bool dpiChanged = _os == null && _SetDpi(); //print.it(dpiChanged, OwnerWindow); var (r, prevSize) = _GetCachedOwnerRect(); //print.it(r, Anchor, _xy, Size); var swp = SWPFlags.NOZORDER | SWPFlags.NOOWNERZORDER | SWPFlags.NOACTIVATE; var bounds = _w.Rect; int x, y, cx = bounds.Width, cy = bounds.Height; if (Anchor.HasLeft()) { x = (Anchor.OppositeX() ? r.right : r.left) + _Scale(_offsets.Left, true); if (Anchor.HasRight() && (!_followedOnce || r.Width != prevSize.width)) { if (_preferSize) _offsets.Right = _Unscale(r.right - x - cx, true); else cx = Math.Max(r.right - _Scale(_offsets.Right, true) - x, 2); //_WmWindowPosChanging will limit min max if need } } else { Debug.Assert(Anchor.HasRight()); x = (Anchor.OppositeX() ? r.left : r.right) - _Scale(_offsets.Right, true) - cx; } if (Anchor.HasTop()) { y = (Anchor.OppositeY() ? r.bottom : r.top) + _Scale(_offsets.Top, true); if (Anchor.HasBottom() && (!_followedOnce || r.Height != prevSize.height)) { if (_preferSize) _offsets.Bottom = _Unscale(r.bottom - y - cy, true); else cy = Math.Max(r.bottom - _Scale(_offsets.Bottom, true) - y, 2); } } else { Debug.Assert(Anchor.HasBottom()); y = (Anchor.OppositeY() ? r.top : r.bottom) - _Scale(_offsets.Bottom, true) - cy; } if (_preferSize) { _preferSize = false; _sett.offsets = _offsets; } if (x == bounds.left && y == bounds.top) swp |= SWPFlags.NOMOVE; if (cx == bounds.Width && cy == bounds.Height) swp |= SWPFlags.NOSIZE; if (!swp.Has(SWPFlags.NOMOVE | SWPFlags.NOSIZE)) { _ignorePosChanged = (byte)(dpiChanged ? 2 : 1); _w.SetWindowPos(swp, x, y, cx, cy); _ignorePosChanged = 0; } bool followedOnce = _followedOnce; _followedOnce = true; if (dpiChanged) { _AutoSizeNowIfIsOpen(measureText: true); } if (!followedOnce && _os != null) { var sc = screen.of(_w, SODefault.Zero); if (sc != _os.Screen) { _w.EnsureInScreen(_os.Screen, workArea: !_w.IsTopmost); } } } bool _followedOnce; byte _ignorePosChanged; bool _preferSize; void _WmWindowPosChanging(ref Api.WINDOWPOS wp) { //uncomment if using properties MinimumSize and MaximumSize. if (!_created) return; ////print.it(this, wp.flags); //if(!wp.flags.Has(SWPFlags.NOSIZE)) { // SIZE min = _GetMinSize(); // if(wp.cx < min.width) wp.cx = min.width; // if(wp.cy < min.height) wp.cy = min.height; // SIZE max = _Scale(MaximumSize); // if(max.width > 0) wp.cx = Math.Min(wp.cx, Math.Max(max.width, min.width)); // if(max.height > 0) wp.cy = Math.Min(wp.cy, Math.Max(max.height, min.height)); //} // //SIZE _GetMinSize() //{ // int k = Border < TBBorder.ThreeD ? 1 : WndUtil.BorderWidth(_w);//? // k *= 2; // var ms = _Scale(MinimumSize); // return (Math.Max(k, ms.Width), Math.Max(k, ms.Height)); //} //don't allow to move the satellite away from the planet. // Only when _inMoveSize. In other cases can create problems, eg when DPI changes. if (!wp.flags.Has(SWPFlags.NOMOVE) && _IsSatellite && _inMoveSize) { RECT r = _satPlanet._w.Rect, rs = _w.Rect; if (wp.cx == rs.Width && wp.cy == rs.Height) { //only when moving. When resizing, it could collapse the toolbar; will snap finally. if (wp.x > r.right) wp.x = r.right; else wp.x = Math.Max(wp.x, r.left - wp.cx); if (wp.y > r.bottom) wp.y = r.bottom; else wp.y = Math.Max(wp.y, r.top - wp.cy); } } } void _WmWindowPosChanged(in Api.WINDOWPOS wp) { if (!_created) return; if (!wp.flags.Has(SWPFlags.NOMOVE | SWPFlags.NOSIZE) && _ignorePosChanged < 2) { bool resized = !wp.flags.Has(SWPFlags.NOSIZE); if (_ignorePosChanged == 0) { if (_os != null) { if (_os.IsScreenInvalid()) return; _os.UpdateIfAutoScreen(); } _UpdateOffsets(wp.x, wp.y, wp.cx, wp.cy); //tested: if SWP_NOMOVE or SWP_NOSIZE, wp contains current values } else { if (resized && !_inMoveSize) _sett.size = _Unscale(_w.ClientRect.Size); } if (resized) { /*SIZE z=*/ _Measure(_w.ClientRect.Width); //rewrap buttons etc //if(AutoSize && _ignorePosChanged && z.height!=wp.cy && Anchor is TBAnchor.TopLR or TBAnchor.BottomLR && Layout==TBLayout.HorizontalWrap) _Resize(z);//rejected } _SatFollow(); } if (wp.flags.Has(SWPFlags.HIDEWINDOW)) { _SatHide(); } } void _UpdateOffsets(int x, int y, int cx, int cy) { var (r, _) = _GetCachedOwnerRect(); //print.it(x, y, cx, cy, r); if (Anchor.HasLeft()) _offsets.Left = _Unscale(x - (Anchor.OppositeX() ? r.right : r.left), true); if (Anchor.HasRight()) _offsets.Right = _Unscale((Anchor.OppositeX() ? r.left : r.right) - x - cx, true); if (Anchor.HasTop()) _offsets.Top = _Unscale(y - (Anchor.OppositeY() ? r.bottom : r.top), true); if (Anchor.HasBottom()) _offsets.Bottom = _Unscale((Anchor.OppositeY() ? r.top : r.bottom) - y - cy, true); if (!_inMoveSize) { _sett.offsets = _offsets; _sett.size = _Unscale(_w.ClientRect.Size); } } void _SetInMoveSize(bool start) { _inMoveSize = start; if (!start) { _sett.offsets = _offsets; var z = _Unscale(_w.ClientRect.Size); if (z != _sett.size) { _sett.size = z; _sett.wrapWidth = z.Width - _BorderPadding(unscaled: true) * 2 + 2; if (AutoSize) _AutoSizeNowIfIsOpen(); } } } bool _inMoveSize; /// /// Gets the cached rectangle of the owner window, screen, control, etc. /// If is owned and anchor has flag Screen, the rectangle is of toolbar's screen. /// Also gets previous size. /// The values are cached by UpdateRect of _OwnerWindow etc. /// (RECT r, SIZE prevSize) _GetCachedOwnerRect() { if (_os != null) return (_os.cachedRect, _os.prevSize); if (_oc != null) return (_oc.cachedRect, _oc.prevSize); return _ow.GetCachedRectAndPrevSize(this); } unsafe nint _WmGetDpiScaledSize(nint wParam, nint lParam) { //a quick and not perfect workaround for: on DPI change sometimes incorrect wrap/autosize if (_os != null && AutoSize && Layout == TBLayout.HorizontalWrap && _inMoveSize) { ref var z = ref *(SIZE*)lParam; int dpi = (int)wParam; z.width = Math2.MulDiv(z.width, dpi, _dpi); z.height = Math2.MulDiv(z.height, dpi, _dpi); z.width += dpi / 8; return 1; } return 0; } unsafe void _WmDpiChanged(nint wParam, nint lParam) { if (_os != null && Math2.LoShort(wParam) != _dpi) { timer.after(1, _ => { //workaround for Win11 bug: when moving the toolbar to another screen, often WM_DPICHANGED received when the window is still in old screen _os.UpdateIfAutoScreen(); if (!_SetDpi()) return; _Images(true); _MeasureText(); if (_inMoveSize && lParam != 0) { //cannot autosize now, or something bad may happen, eg nested wm_dpichanged until stack overflow _w.MoveL(*(RECT*)lParam); } else { if (DpiScaling.offsets == true) _FollowRect(); //update offsets if script wants so _AutoSizeNowIfIsOpen(); } }); } } void _WmDisplayChange() { if (_os != null) { if (_os.IsScreenInvalid()) return; timer.after(200, _ => { if (_os == null || _closed) return; //workaround for Win11 bug: WS_EX_TOOLWINDOW windows don't receive WM_DPICHANGED when screen DPI changes var dpi = _os.Screen.Dpi; if (dpi != _dpi) _WmDpiChanged(dpi, 0); _os.UpdateRect(out bool changed); if (changed) _FollowRect(); }); } else if (osVersion.minWin8_1) { //If owner's screen DPI changed, update toolbar's DPI/size/offsets. Need it for pm-dpi-aware windows that don't change rect when DPI changed. //WM_DISPLAYCHANGE doc: "when the display resolution has changed". Also when changed DPI, although undocumented. Tested on Win8.1 too. // wm_dpichanged isn't good for it. We need DPI of owner, not of toolbar. // Also we receive wm_settingchanged(SPI_SETLOGICALDPIOVERRIDE), but can't trust it. SPI_SETLOGICALDPIOVERRIDE's documentation is "Do not use.". No on Win8.1. //Wait until owner's DPI updated. If owner is pm-dpi-aware, its DPI is updated at random times, maybe after several s. int i = 15, oldDpi = _dpi; timer.every(1000, t => { if (--i > 0 && _dpi == oldDpi && !_closed && !_hide.Has(TBHide.Owner)) { if (screen.of(OwnerWindow).Dpi == oldDpi) return; //print.it("DPI changed", _dpi, Dpi.OfWindow(OwnerWindow, true)); _FollowRect(); } //print.it("stop"); t.Stop(); //tested: if pm-dpi-aware window is minimized, its dpi changes when restored. Not if hidden. }); } } } //TODO3: now toolbars are lost too often. // Eg after removing autohide. ================================================ FILE: Au/GUI/toolbar/tb render.cs ================================================ using System.Drawing; using System.Drawing.Drawing2D; namespace Au; public partial class toolbar { /// /// Border style. /// Default . /// /// /// This property is in the context menu and is saved. /// public TBBorder Border { get => _sett.border; set { _ThreadTrap(); if (value == _sett.border) return; if (IsOpen) { RECT r = _w.ClientRectInScreen; var bp1 = _BorderPadding(); _sett.border = value; var bpDiff = _BorderPadding() - bp1; if (bpDiff != 0) { r.Inflate(bpDiff, bpDiff); r.Normalize(false); } const WS mask = WS.CAPTION | WS.THICKFRAME | WS.SYSMENU; WS s1 = _w.Style, s2 = _BorderStyle(value); if (s2 != (s1 & mask)) _w.SetStyle(s1 = ((s1 & ~mask) | s2)); Dpi.AdjustWindowRectEx(r, ref r, s1, _w.ExStyle); _w.MoveL(r, SWPFlags.FRAMECHANGED | SWPFlags.HIDEWINDOW); if (bpDiff != 0) _Measure(); //update button rectangles _w.ShowL(true); } else { _sett.border = value; } } } /// /// Layout of buttons (horizontal, vertical). /// /// /// This property is in the context menu and is saved. /// public TBLayout Layout { get => _sett.layout; set { _ThreadTrap(); if (value != _sett.layout) { _sett.layout = value; _AutoSizeNowIfIsOpen(); } } } /// /// Display button text. Default true. If false, displays text in tooltips, and only displays first 2 characters for buttons without image. /// /// /// A button can override this property: /// - To never display text, let its text be empty, like "" or "|Tooltip". /// - To always display text, append "\a", like "Text\a" or "Text\a|Tooltip". /// public bool DisplayText { get => _sett.dispText; set { _ThreadTrap(); if (value != _sett.dispText) { _sett.dispText = value; _AutoSizeNowIfIsOpen(measureText: true); } } } /// /// Font properties. /// /// /// Cannot be changed after showing toolbar window. /// public FontNSS Font { get => _font ??= new(); set { _font = value; } } FontNSS _font; /// /// Text color. /// If default, uses system color. /// public ColorInt TextColor { get => _textColor; set { _textColor = value; _Invalidate(); } } ColorInt _textColor; /// /// Background color or brush. /// /// Can be , , int (color 0xRRGGBB) or . If null (default), uses system color. public object Background { get => _background; set { if (value is not (null or Brush or Color or ColorInt or int)) throw new ArgumentException(); _background = value; _Invalidate(); } } object _background; /// /// Border color when is ... . /// If default, uses system color. /// public ColorInt BorderColor { get => _borderColor; set { _borderColor = value; _Invalidate(); } } ColorInt _borderColor; /// /// Don't display the default image (dot or triangle). /// public bool NoDefaultImage { get; set; } /// /// Sets some metrics of this toolbar, for example button padding. /// /// /// Cannot be changed after showing toolbar window. /// /// /// /// /// public TBMetrics Metrics { get; set; } /// /// Sets default metrics of toolbars. /// /// public static TBMetrics defaultMetrics { get => s_defaultMetrics ??= new(); set { s_defaultMetrics = value; } } static TBMetrics s_defaultMetrics; void _Images(bool onDpiChanged) { foreach (var v in _a) { if (onDpiChanged) { if (v.image2 == null || v.image is not string) continue; //will either find/return same bitmap in cache, or create new bitmap from XAML, or return found old XAML bitmap created for other DPI. //note: will not reload non-XAML images, eg those created from native icons. They will be drawn DPI-scaled, slightly blurry. } //var old = v.image2; v.image2 = _GetImage(v).image; //if (onDpiChanged) print.it(old == v.image2); if (!_hasCachedImages && !onDpiChanged && v.image2 != null && v.image is string s && s.Length > 0) _hasCachedImages = true; } if (_hasCachedImages) _ImageCache.Cleared += _UpdateCachedImages; } bool _hasCachedImages; void _UpdateCachedImages() { foreach (var v in _a) { if (v.image2 != null && v.image is string s && s.Length > 0) { if (_GetImage(v).image is { } b) v.image2 = b; } } } //not used //internal void ChangeImage_(TBItem ti, Bitmap b) { // if (_closed) return; // ti.image2 = b; // _Invalidate(ti); //} const TFFlags c_tff = TFFlags.NOPREFIX | TFFlags.EXPANDTABS; void _MeasureText() { NativeFont_ font = null; FontDC_ dc = null; try { foreach (var b in _a) { int len = _TextDispLen(b); if (len != 0) { dc ??= new FontDC_(font = Font.CreateFont(_dpi)); b.textSize = dc.MeasureDT(b.Text.AsSpan(0, len), c_tff); b.textSize.height++; } else { b.textSize = default; } } } finally { dc?.Dispose(); font?.Dispose(); } } int _TextDispLen(TBItem b) { var s = b.Text; if (s.NE()) return 0; if (DisplayText || b.textAlways) return s.Length; if (b.HasImage_) return 0; return Math.Min(s.Length, 2); //info: 2 is ok for surrogate } struct _Metrics { public int bBorder, bPaddingX, bPaddingY, tbBorder, tbPadding, textPaddingR, textPaddingY, image, dot, triangle, imagePaddingX; public _Metrics(toolbar tb) { var k = tb.Metrics ?? defaultMetrics; int dpi = tb._dpi; bBorder = dpi / 96; bPaddingX = Dpi.Scale(k.ButtonPaddingX, dpi); bPaddingY = Dpi.Scale(k.ButtonPaddingY, dpi); tbPadding = tb._BorderPadding(); tbBorder = tbPadding > 0 ? bBorder : 0; textPaddingR = Dpi.Scale(4, dpi); textPaddingY = Dpi.Scale(1, dpi); image = Dpi.Scale(tb.ImageSize, dpi); if (!tb.NoDefaultImage) { dot = tb.ImageSize / 3; triangle = tb.ImageSize / 2; } else dot = triangle = 0; imagePaddingX = Dpi.Scale(2, dpi); //tbBorder += 1; //test border thickness //bBorder += 1; } public int ImageEtc(TBItem b, bool vert) => b.HasImage_ ? image : (dot == 0 ? 0 : (vert ? image : (b.IsMenu_ ? triangle : dot))); } /// /// Measures toolbar size and sets button rectangles. /// Returns size of client area. /// SIZE _Measure(int? width = null) { //print.it("measure"); SIZE R = default; bool autoSize = AutoSize && _a.Count > 0; bool vert = Layout == TBLayout.Vertical; var m = new _Metrics(this); int tbp = m.tbPadding, buttonPlusX = (m.bBorder + m.bPaddingX + m.imagePaddingX) * 2, buttonPlusY = (m.bBorder + m.bPaddingY + m.textPaddingY) * 2; int ww = int.MaxValue; if (width != null) ww = width.Value - tbp * 2; else if (!autoSize) ww = _Scale(_sett.size.Width, false) - tbp * 2; else if (AutoSizeWrapWidth > 0) ww = _Scale(AutoSizeWrapWidth, false); //ww = Math.Max(ww, m.image); //no, then can't have thin vertical screen edge toolbars for (int i = 0, x = 0, y = 0; i < _a.Count; i++) { var b = _a[i]; SIZE z; if (b.IsGroup_ || (b.IsSeparator_ && vert)) { _NewRow(); if (b.Text.NE()) z = new(2, 2 + m.imagePaddingX * 2); else z = new(b.textSize.width + m.image * 2, b.textSize.height + m.imagePaddingX * 2); } else { if (b.IsSeparator_) { z = new(2 + m.imagePaddingX * 2, _a[i - 1].rect.Height); } else { z = new(buttonPlusX + m.ImageEtc(b, vert), m.image + m.bPaddingY * 2 + m.bBorder * 4); //m.bBorder*4 for borders and image padding if (b.textSize.width > 0) { z.width += b.textSize.width + m.textPaddingR; z.height = Math.Max(z.height, b.textSize.height + buttonPlusY); } } if (!vert) if (x + z.width > ww && x > 0) _NewRow(); } b.rect = new(x, y, z.width, z.height); b.rect.right = Math.Min(b.rect.right, ww); x += z.width; R.width = Math.Max(R.width, x); R.height = Math.Max(R.height, b.rect.bottom); if (vert || b.IsGroup_) _NewRow(); void _NewRow() { x = 0; y = R.height; } } if (autoSize) { R.width = Math.Min(R.width, ww) + tbp * 2; R.height += tbp * 2; } else { R = new(ww + tbp * 2, _Scale(_sett.size.Height, false)); } //print.it(R); foreach (var b in _a) { b.rect.Offset(tbp, tbp); if (vert || b.IsGroup_) b.rect.right = R.width - tbp; } return R; } void _WmPaint(IntPtr dc, Graphics g, RECT rClient, RECT rUpdate) { switch (Background) { case Brush bb: g.FillRectangle(bb, rUpdate); break; case Color bc: g.Clear(bc); break; case ColorInt bc: g.Clear((Color)bc); break; case int bc: g.Clear(bc.ToColor_()); break; default: g.Clear(SystemColors.Control); break; } var m = new _Metrics(this); bool vert = Layout == TBLayout.Vertical; Brush brushDot = null, brushTriangle = null; NativeFont_ font = null; IntPtr oldFont = default; int groupTextX = 0; int sysTextColor = Api.GetSysColor(Api.COLOR_BTNTEXT); try { for (int i = 0; i < _a.Count; i++) { var b = _a[i]; if (!b.rect.IntersectsWith(rUpdate)) continue; //g.DrawRectangleInset(Pens.BlueViolet, b.rect); int textColor = b.TextColor != default ? b.TextColor.ToBGR() : (TextColor != default ? TextColor.ToBGR() : sysTextColor); int xImage = b.rect.left + m.bBorder + m.bPaddingX + m.imagePaddingX; if (b.IsSeparator_ && !vert) { var r = b.rect; r.Inflate(-m.imagePaddingX, -m.imagePaddingX - m.bBorder); Api.DrawEdge(dc, ref r, Api.EDGE_ETCHED, Api.BF_LEFT); } else if (b.IsSeparatorOrGroup_) { int pad = m.tbPadding + m.imagePaddingX; RECT r = new(pad, b.rect.CenterY - 1, rClient.Width - pad * 2, 2); if (b.Text.NE()) { Api.DrawEdge(dc, ref r, Api.EDGE_ETCHED, Api.BF_TOP); } else { int len = (r.Width - b.textSize.width) / 2 - m.textPaddingR; //length of left and right (from text) parts if (len > 0) { r.left = r.right - len; Api.DrawEdge(dc, ref r, Api.EDGE_ETCHED, Api.BF_TOP); //right r.left = pad; r.right = pad + len; Api.DrawEdge(dc, ref r, Api.EDGE_ETCHED, Api.BF_TOP); //left groupTextX = r.right + m.textPaddingR; } else groupTextX = pad; } } else { if (i == _iHot || i == _iClick) { if (!_noHotClick && (textColor == 0 || TextColor != default || b.TextColor != default)) g.FillRectangle((i == _iClick ? 0xC0DCF3 : 0xD8E6F2).ToColor_(), b.rect); g.DrawRectangleInset((i == _iClick ? 0x90C8F6 : 0xC0DCF3).ToColor_(), m.bBorder, b.rect); } int x = xImage, y = b.rect.top; if (!b.HasImage_) { if (m.dot > 0) { g.SmoothingMode = SmoothingMode.HighQuality; y += (b.rect.Height - m.dot) / 2; if (vert) x += m.image / 4; if (b.IsMenu_) { y += m.triangle / 6; if (vert) x -= m.triangle / 8; brushTriangle ??= new SolidBrush(Color.YellowGreen); g.FillPolygon(brushTriangle, new Point[] { new(x, y), new(x + m.triangle, y), new(x + m.triangle / 2, y + m.triangle / 2) }); } else { brushDot ??= new SolidBrush(Color.SkyBlue); g.FillEllipse(brushDot, x, y, m.dot, m.dot); } g.SmoothingMode = SmoothingMode.None; } } else if (b.image2 != null) { try { g.DrawImage(b.image2, x, y + (b.rect.Height - m.image) / 2, m.image, m.image); } catch (Exception ex) { Debug_.Print(ex); } } } if (b.textSize.width > 0) { if (font == null) { font = Font.CreateFont(_dpi); oldFont = Api.SelectObject(dc, font.Handle); Api.SetBkMode(dc, 1); } Api.SetTextColor(dc, textColor); RECT r = new(b.IsGroup_ ? groupTextX : xImage + m.ImageEtc(b, vert) + m.imagePaddingX, b.rect.top + (b.rect.Height - b.textSize.height) / 2, b.textSize.width, b.rect.Height); r.right = Math.Min(r.right, b.rect.right - m.bBorder * 2); Api.DrawText(dc, b.Text.AsSpan(0, _TextDispLen(b)), ref r, c_tff); } } } finally { brushDot?.Dispose(); brushTriangle?.Dispose(); if (font != null) { Api.SelectObject(dc, oldFont); font.Dispose(); } } if (m.tbBorder > 0) { g.DrawRectangleInset(BorderColor != default ? (Color)BorderColor : SystemColors.ControlDark, m.tbBorder, rClient); } } } ================================================ FILE: Au/GUI/toolbar/tb sat.cs ================================================ using Au.Triggers; namespace Au; public partial class toolbar { toolbar _satellite; toolbar _satPlanet; bool _satVisible; int _satAnimation; timer _satTimer; timer _satAnimationTimer; screen _screenAHSE; /// /// A toolbar attached to this toolbar. Can be null. /// /// The set function throws this exception if the satellite toolbar was attached to another toolbar or was shown as non-satellite toolbar. /// /// The satellite toolbar is shown when mouse enters its owner toolbar and hidden when mouse leaves it and its owner. Like an "auto hide" feature. /// A toolbar can have multiple satellite toolbars at different times. A satellite toolbar can be attached/detached multiple times to the same toolbar. /// public toolbar Satellite { get => _satellite; set { _ThreadTrap(); if (value != _satellite) { if (value == null) { _SatHide(); _satellite = null; //and don't clear _satPlanet etc } else { if (_closed || value._closed) throw new ObjectDisposedException(nameof(toolbar)); var p = value._satPlanet; if (p != this) { if (p != null || value._created) throw new InvalidOperationException(); } _satellite = value; _satellite._satPlanet = this; } } } } /// /// If this is a satellite toolbar (), gets its owner toolbar. Else null. /// public toolbar SatelliteOwner => _satPlanet; bool _IsSatellite => _satPlanet != null; toolbar _SatPlanetOrThis => _satPlanet ?? this; void _SatMouse() { if (_satellite == null || _satVisible) return; _satVisible = true; //print.it("show"); if (!_satellite._created) { var owner = _w; _satellite._CreateWindow(true, owner, isSatelite: true); _satellite._ow = new _OwnerWindow(owner); _satellite._ow.a.Add(_satellite); } _SatFollow(); _SatShowHide(true, animate: true); _satTimer ??= new timer(_SatTimer); _satTimer.Every(100); } void _SatTimer(timer _) { Debug.Assert(IsOpen); if (_inMoveSize || _satellite._inMoveSize) return; POINT p = mouse.xy; int dist = Dpi.Scale(30, _dpi); var wa = wnd.active; RECT ru = default; if (_MouseIsIn(this) || _MouseIsIn(_satellite)) return; if (ru.Contains(p)) return; if (miscInfo.getGUIThreadInfo(out var g, Api.GetCurrentThreadId()) && g.flags.Has(GTIFlags.INMENUMODE)) return; bool _MouseIsIn(toolbar tb) { var w = tb._w; if (w == wa) return true; var r = w.Rect; r.Inflate(dist, dist); if (r.Contains(p.x, p.y)) return true; ru.Union(r); return false; } _SatHide(animate: true); } void _SatDestroying() { if (_IsSatellite) _satPlanet.Satellite = null; Debug_.PrintIf(_satellite != null && _satellite.IsOpen, "_satellite"); //When destroying planet, OS at first destroys satellites (owned windows). } //Hides _satellite and stops _satTimer. void _SatHide(bool animate = false/*, [CallerMemberName] string cmn=null*/) { if (_satellite == null) return; //print.it("hide", cmn, _satVisible); if (_satVisible) { _satVisible = false; _satTimer.Stop(); _SatShowHide(false, animate); } else if (!animate && (_satAnimationTimer?.IsRunning ?? false)) { _SatShowHide(false, false); } } //Shows or hides _satellite and manages animation. void _SatShowHide(bool show, bool animate) { if (!animate || _satellite._transparency != default) { var w = _satellite._w; if (show != w.IsVisible) _satellite._SetVisibleL(show); if (_satellite._transparency == default) w.SetTransparency(false); _satAnimationTimer?.Stop(); _satAnimation = 0; return; } _satAnimationTimer ??= new timer(_ => { _satAnimation += _satVisible ? 64 : -32; bool stop; if (_satVisible) { if (stop = _satAnimation >= 255) _satAnimation = 255; } else { if (stop = _satAnimation <= 0) _satAnimation = 0; } if (stop) { _satAnimationTimer.Stop(); if (_satAnimation == 0) _satellite._SetVisibleL(false); } if (_satellite._transparency == default) _satellite._w.SetTransparency(!stop, _satAnimation); }); _satAnimationTimer.Now(); _satAnimationTimer.Every(30); if (show) _satellite._SetVisibleL(true); } void _SatFollow() { if (!_satVisible) return; if (!_satellite._ow.UpdateRect(out bool changed) || !changed) return; _satellite._FollowRect(onFollowOwner: true); } #region auto-hide owner toolbars /// /// Creates a new toolbar and sets its = this. /// /// The new toolbar. /// constructor flags. /// [](xref:caller_info) /// [](xref:caller_info) /// This toolbar was attached to another toolbar or was shown as non-satellite toolbar. /// /// Sets toolbar name = this.Name + "^". /// If this already is a satellite toolbar, just returns its owner. /// public toolbar AutoHide(TBCtor ctorFlags = 0, [CallerFilePath] string f_ = null, [CallerLineNumber] int l_ = 0) { _ThreadTrap(); if (_satPlanet == null) { _satPlanet = new toolbar(this.Name + "^", ctorFlags, f_, l_) { Satellite = this }; if (_satPlanet.FirstTime) { _satPlanet.Size = new(20, 20); } _satPlanet.AutoSize = false; } return _satPlanet; } /// /// Creates a new toolbar and sets its = this. Sets properties for showing at a screen edge. /// /// The new toolbar. /// Mouse edge trigger arguments. /// rangeStart and rangeEnd can be used to specify a smaller range of the edge part. For example, you can create 2 toolbars there: one with 0, .5f, other with .5f, 1f. /// /// The visible thickness. Pixels. /// constructor flags. /// [](xref:caller_info) /// [](xref:caller_info) public toolbar AutoHideScreenEdge(MouseTriggerArgs mta, Coord rangeStart = default, Coord rangeEnd = default, int thickness = 1, TBCtor ctorFlags = 0, [CallerFilePath] string f_ = null, [CallerLineNumber] int l_ = 0) { Not_.Null(mta); if (mta.Trigger.Kind != TMKind.Edge) throw new ArgumentException("Not an edge trigger."); return AutoHideScreenEdge(mta.Trigger.Edge, mta.Trigger.Screen, rangeStart, rangeEnd, thickness, ctorFlags, f_, l_); } /// Screen edge/part. /// Screen. Default: primary. /// public toolbar AutoHideScreenEdge(TMEdge edge, screen scrn = default, Coord rangeStart = default, Coord rangeEnd = default, int thickness = 1, TBCtor ctorFlags = 0, [CallerFilePath] string f_ = null, [CallerLineNumber] int l_ = 0) { _ThreadTrap(); var sh = scrn.Now; var rs = sh.Rect; var se = edge.ToString(); char se0 = se[0]; bool vertical = se0 == 'L' || se0 == 'R'; TBAnchor anchor = TBAnchor.TopLeft; RECT k = default; if (thickness <= 0) thickness = 1; int offscreen = thickness == 1 ? 1 : 0; switch (se0) { case 'T': k.top = -offscreen; break; case 'R': k.right = -offscreen; anchor = TBAnchor.TopRight; break; case 'B': k.bottom = -offscreen; anchor = TBAnchor.BottomLeft; break; case 'L': k.left = -offscreen; break; } int x25 = rs.Width / 4, y25 = rs.Height / 4; bool reverse = false; switch (edge) { case TMEdge.TopInCenter50: k.left = x25; break; case TMEdge.TopInRight25: anchor = TBAnchor.TopRight; reverse = true; break; case TMEdge.RightInCenter50: k.top = y25; break; case TMEdge.RightInBottom25: anchor = TBAnchor.BottomRight; reverse = true; break; case TMEdge.BottomInCenter50: k.left = x25; break; case TMEdge.BottomInRight25: anchor = TBAnchor.BottomRight; reverse = true; break; case TMEdge.LeftInCenter50: k.top = y25; break; case TMEdge.LeftInBottom25: anchor = TBAnchor.BottomLeft; reverse = true; break; } int edgeLength = vertical ? rs.Height : rs.Width; if (se.Contains("25")) edgeLength /= 4; else if (se.Contains("50")) edgeLength /= 2; int move = rangeStart.IsEmpty ? 0 : rangeStart.NormalizeInRange(0, edgeLength); int length = rangeEnd.IsEmpty ? edgeLength : Math.Max(0, rangeEnd.NormalizeInRange(0, edgeLength) - move); if (vertical) { if (reverse) k.bottom = edgeLength - length - move; else k.top += move; } else { if (reverse) k.right = edgeLength - length - move; else k.left += move; } var planet = AutoHide(ctorFlags, f_, l_); planet._screenAHSE = sh; if (planet.FirstTime || vertical != (planet.Size.Height > planet.Size.Width)) { SIZE z1 = vertical ? new(thickness + offscreen, length) : new(length, thickness + offscreen); //planet.Size = Dpi.Unscale(z1, sh.Handle); //no, such toolbars aren't DPI-sceled by default planet.Size = z1; } if (planet.FirstTime) { planet.Sizable = false; } planet.Anchor = anchor; planet.Offsets = new(k.left, k.top, k.right, k.bottom); planet.Border = TBBorder.Width1; planet.NoContextMenu = TBNoMenu.Anchor | TBNoMenu.Border | TBNoMenu.Layout | TBNoMenu.AutoSize; this.Anchor = anchor | (vertical ? TBAnchor.OppositeEdgeX : TBAnchor.OppositeEdgeY); this.NoContextMenu = TBNoMenu.Anchor; return planet; } #endregion } ================================================ FILE: Au/GUI/toolbar/tb types.cs ================================================ namespace Au.Types; /// /// Represents a button or separator in . /// /// /// Most properties cannot be changed while the toolbar is open. Can be changed , . /// public class TBItem : MTItem { readonly toolbar _tb; internal SIZE textSize; internal readonly TBItemType type; internal bool textAlways; internal popupMenu menu; internal TBItem(toolbar tb, TBItemType type) { _tb = tb; this.type = type; textAlways = type == TBItemType.Group; } internal bool IsSeparator_ => type == TBItemType.Separator; internal bool IsGroup_ => type == TBItemType.Group; internal bool IsMenu_ => type == TBItemType.Menu; internal bool IsSeparatorOrGroup_ => type is TBItemType.Separator or TBItemType.Group; /// public TBItemType ItemType => type; /// Gets button action. public Action Clicked => base.clicked as Action; /// /// Button text. /// /// /// Changing button text at run time resizes the button and toolbar. /// public new string Text { get => base.Text; set { _tb._ThreadTrap(); if (value != base.Text) { base.Text = value; _tb._AutoSizeNowIfIsOpen(measureText: true); } } } } /// /// Used with . /// public enum TBItemType : byte { #pragma warning disable 1591 //doc Button, Menu, Separator, Group, #pragma warning restore } /// /// Used with . /// [Flags] public enum TBFlags { /// /// Activate the owner window when the toolbar clicked. Default. /// ActivateOwnerWindow = 1, /// /// Hide the toolbar when a full-screen window is active. Default. /// HideWhenFullScreen = 2, //rejected: use SHQueryUserNotificationState to detect also presentation mode etc. Too slow, eg 1300 mcs, which is 100-500 times slower than wnd.isFullScreen. // HideWhenFullScreenActive // HideWhenFullScreenRunning (QUNS_BUSY) (in primary screen only) // HideWhenPresentation (QUNS_PRESENTATION_MODE) // HideWhenD3DFullScreen (QUNS_RUNNING_D3D_FULL_SCREEN). I guess it is for games. Not tested. // But problem: no QUNS_BUSY if fullscreen in non-primary screen. // Also found this comment (not tested): "It also only works when the Explorer shell is running." } /// /// Used with . /// public enum TBBorder { //note: don't reorder. /// No border. None, /// 1 pixel border. Width1, /// 1 pixel border + 1 pixel padding. Width2, /// 1 pixel border + 2 pixels padding. Width3, /// 1 pixel border + 3 pixels padding. Width4, /// 3D border. ThreeD, /// Standard window border. Thick, /// Title bar and standard window border. Caption, /// Title bar, X button and standard window border. CaptionX, } /// /// Used with . /// public enum TBAnchor { //top 1, bottom 2, left 4, right 8 /// /// Anchors are top and left edges. Default. /// TopLeft = 1 | 4, /// /// Anchors are top and right edges. /// TopRight = 1 | 8, /// /// Anchors are bottom and left edges. /// BottomLeft = 2 | 4, /// /// Anchors are bottom and right edges. /// BottomRight = 2 | 8, /// /// Anchors are top, left and right edges. The toolbar is resized horizontally when resizing its owner. /// TopLR = 1 | 4 | 8, /// /// Anchors are bottom, left and right edges. The toolbar is resized horizontally when resizing its owner. /// BottomLR = 2 | 4 | 8, /// /// Anchors are left, top and bottom edges. The toolbar is resized vertically when resizing its owner. /// LeftTB = 4 | 1 | 2, /// /// Anchors are right, top and bottom edges. The toolbar is resized vertically when resizing its owner. /// RightTB = 8 | 1 | 2, /// /// Anchors are all edges. The toolbar is resized when resizing its owner. /// All = 15, /// /// Use owner's opposite left/right edge than specified. In other words, attach toolbar's left edge to owner's right edge or vice versa. /// This flag is for toolbars that normally are outside of the owner rectangle (at the left or right). /// This flag cannot be used with TopLR, BottomLR, All. /// OppositeEdgeX = 32, /// /// Use owner's opposite top/bottom edge than specified. In other words, attach toolbar's top edge to owner's bottom edge or vice versa. /// This flag is for toolbars that normally are outside of the owner rectangle (above or below). /// This flag cannot be used with LeftTB, RightTB, All. /// OppositeEdgeY = 64, /// /// Anchor is screen, not owner window. Don't move the toolbar together with its owner window. /// Screen = 128, } static partial class TBExt_ { internal static bool HasTop(this TBAnchor a) => 0 != ((int)a & 1); internal static bool HasBottom(this TBAnchor a) => 0 != ((int)a & 2); internal static bool HasLeft(this TBAnchor a) => 0 != ((int)a & 4); internal static bool HasRight(this TBAnchor a) => 0 != ((int)a & 8); internal static bool OppositeX(this TBAnchor a) => 0 != ((int)a & 32); internal static bool OppositeY(this TBAnchor a) => 0 != ((int)a & 64); internal static bool OfScreen(this TBAnchor a) => 0 != ((int)a & 128); internal static TBAnchor WithoutFlags(this TBAnchor a) => a & TBAnchor.All; } //rejected. Instead use System.Windows.Size. It loads 1 assembly in 1.5 ms and does not add much process memory. //public record struct SizeD //{ // [System.Text.Json.Serialization.JsonInclude] // public double width; // [System.Text.Json.Serialization.JsonInclude] // public double height; // public SizeD(double width, double height) { this.width = width; this.height = height; } //} /// /// Used with . /// public record struct TBOffsets { /// /// Horizontal distance from the owner's left edge (right if ) to the toolbar's left edge. /// public double Left { get; set; } /// /// Vertical distance from the owner's top edge (bottom if ) to the toolbar's top edge. /// public double Top { get; set; } /// /// Horizontal distance from the toolbar's right edge to the owner's right edge (left if ). /// public double Right { get; set; } /// /// Vertical distance from the toolbar's bottom edge to the owner's bottom edge (top if ). /// public double Bottom { get; set; } /// /// Sets all properties. /// public TBOffsets(double left, double top, double right, double bottom) { Left = left; Top = top; Right = right; Bottom = bottom; } } /// /// Reasons to hide a toolbar. Used with . /// [Flags] public enum TBHide { /// Owner window is hidden, minimized, etc. Owner = 1, /// A full-screen window is active. See flag . FullScreen = 2, //Satellite = 128, //no, _SetVisible and this enum aren't used with satellites /// This and bigger flag values can be used by callers for any purpose. Value 0x10000. User = 0x10000, } /// /// Used with . /// public enum TBLayout { /// Default layout. Buttons are in single row. Wrapped when exceeds maximal row width. More rows can be added with . HorizontalWrap, /// Buttons are in single column, like in a popup menu. Separators are horizontal. Vertical, //TODO3: if some buttons don't fit, add overflow drop-down menu. Or scrollbar; or add VerticalScroll. // /// Buttons are in single row. When it exceeds maximal row width, buttons are moved to a drop-down menu. More rows can be added with . // Horizontal,//TODO3 } /// /// Used with . /// [Flags] public enum TBNoMenu { #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member Menu = 1, Edit = 1 << 1, Anchor = 1 << 2, Layout = 1 << 3, Border = 1 << 4, Sizable = 1 << 5, AutoSize = 1 << 6, MiscFlags = 1 << 7, Toolbars = 1 << 8, Help = 1 << 9, Close = 1 << 10, File = 1 << 11, Text = 1 << 12, #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member } /// /// Flags for constructor. /// [Flags] public enum TBCtor { /// /// Don't load saved settings. Delete the settings file of the toolbar, if exists. /// ResetSettings = 1, /// /// Don't load and save settings. No file will be created or opened. /// DontSaveSettings = 2, } /// /// Used with . /// public struct TBScaling { /// public TBScaling(bool? size, bool? offsets) { this.size = size; this.offsets = offsets; } /// /// Scale toolbar size and related properties. /// If default (null), scales size, except of empty toolbars created by . /// public bool? size; /// /// Scale toolbar offsets. See . /// If default (null), scales offsets, except when anchor is screen (not window etc). /// public bool? offsets; } /// /// Used with and . /// /// Button padding left and right. /// Button padding top and bottom. /// /// All values are in logical pixels (1 pixel when DPI is 100%). /// public record class TBMetrics(int ButtonPaddingX = 0, int ButtonPaddingY = 0); /// /// Used with . /// /// /// Allows a toolbar to follow an object in the owner window, for example a UI element or image. Or to hide in certain conditions. /// Define a class that implements this interface. Create a variable of that class and pass it to . /// The interface functions are called every 250 ms, sometimes more frequently. Not called when the owner window is invisible or cloaked or minimized. /// public interface ITBOwnerObject { /// /// Returns false to close the toolbar. /// /// /// Not called if the owner window is invisible or cloaked or minimized. /// The default implementation returns true. /// bool IsAlive => true; /// /// Returns false to hide the toolbar temporarily. /// /// /// Not called if the owner window is invisible or cloaked or minimized. /// The default implementation returns true. /// bool IsVisible => true; /// /// Gets object rectangle. /// /// false if failed. /// Rectangle in screen coordinates. /// /// Not called if the owner window is invisible or cloaked or minimized or if returned false. /// bool GetRect(out RECT r); } ================================================ FILE: Au/GUI/toolbar/tb util.cs ================================================ namespace Au; public partial class toolbar { #if DEBUG /// /// This is a debug-only function. Returns the caller method name. Use like print.it(caller(), ...). /// static string caller([CallerMemberName] string m_ = null) => m_; #endif bool _SetDpi() { Debug.Assert(_os != null || !OwnerWindow.Is0); return _SetDpi(_os != null ? _os.Screen.Dpi : screen.of(OwnerWindow).Dpi); } bool _SetDpi(int dpi) { if (dpi == _dpi) return false; _dpi = dpi; _dpiF = _dpi / 96d; return true; } bool _NeedScaling(bool offsets) { if (_dpi == 96) return false; if (offsets) return DpiScaling.offsets ?? _os == null; return DpiScaling.size ?? _screenAHSE.IsEmpty; } int _Scale(double d, bool offsets) { if (_NeedScaling(offsets)) d *= _dpiF; return d.ToInt(); } double _Unscale(int i, bool offsets) => _NeedScaling(offsets) ? i / _dpiF : i; SIZE _Scale(System.Windows.Size z) => _NeedScaling(false) ? Dpi.Scale(z, _dpi) : SIZE.From(z, true); System.Windows.Size _Unscale(SIZE z) => _NeedScaling(false) ? Dpi.Unscale(z, _dpi) : z; //System.Windows.Size _Unscale(int width, int height) => _Unscale(new SIZE(width, height)); static double _Limit(double d) { if (double.IsNaN(d)) throw new ArgumentException(); const int c_max = 2_000_000; //for max *1024 DPI scaling return Math.Clamp(d, -c_max, c_max); } /// /// Measures, resizes and invalidates the toolbar now if need. /// internal void _AutoSizeNowIfIsOpen(bool measureText = false) { if (!IsOpen) return; if (measureText) _MeasureText(); _Resize(_Measure()); Api.InvalidateRect(_w); } void _Resize(SIZE clientSize/*, bool ignoreAnchor=false*/) { var r = new RECT(0, 0, clientSize.width, clientSize.height); Dpi.AdjustWindowRectEx(_dpi, ref r, _w.Style, _w.ExStyle); int cx = r.Width, cy = r.Height; var a = Anchor.WithoutFlags(); if (/*ignoreAnchor ||*/ a == TBAnchor.All) { _w.ResizeL(cx, cy); } else { var rw = _w.Rect; int dx = cx - rw.Width, dy = cy - rw.Height; if (!a.HasLeft()) rw.left -= dx; else if (!a.HasRight()) rw.right += dx; if (!a.HasTop()) rw.top -= dy; else if (!a.HasBottom()) rw.bottom += dy; _w.MoveL(rw); } } void _Invalidate(TBItem ti = null) { _ThreadTrap(); if (!IsOpen) return; if (ti != null) Api.InvalidateRect(_w, ti.rect); else Api.InvalidateRect(_w); } void _Invalidate(int i) => _Invalidate(_a[i]); static WS _BorderStyle(TBBorder b) => b switch { TBBorder.ThreeD => WS.DLGFRAME, TBBorder.Thick => WS.THICKFRAME, TBBorder.Caption => WS.CAPTION | WS.THICKFRAME, TBBorder.CaptionX => WS.CAPTION | WS.THICKFRAME | WS.SYSMENU, _ => 0 }; /// /// Returns DPI-scaled border thickness in client area. Returns 0 if b is not TBBorder.Width1 ... TBBorder.Width4. /// static int _BorderPadding(TBBorder b, int dpi) => b >= TBBorder.Width1 && b <= TBBorder.Width4 ? Dpi.Scale((int)b, dpi) : 0; /// /// Returns DPI-scaled border thickness in client area. Returns 0 if b is not TBBorder.Width1 ... TBBorder.Width4. /// int _BorderPadding(TBBorder? b = null, bool unscaled = false) => _BorderPadding(b ?? Border, unscaled ? 96 : _dpi); static TBAnchor _GetInvalidAnchorFlags(TBAnchor anchor) { return anchor.WithoutFlags() switch { TBAnchor.TopLeft or TBAnchor.TopRight or TBAnchor.BottomLeft or TBAnchor.BottomRight => 0, TBAnchor.TopLR or TBAnchor.BottomLR => TBAnchor.OppositeEdgeX, TBAnchor.LeftTB or TBAnchor.RightTB => TBAnchor.OppositeEdgeY, _ => TBAnchor.OppositeEdgeX | TBAnchor.OppositeEdgeY, }; } void _CreatedTrap(string error = null) { if (_created) throw new InvalidOperationException(error); } } ================================================ FILE: Au/GUI/toolbar/toolbar.cs ================================================ using Au.Triggers; namespace Au; //rejected: warn if using common controls version 5. Eg tooltips don't work. The same with menus. // Too many problems: 1. How to warn? Message box? 2. Need to also warn if no supported OS, DPI awareness, etc. Load/parse manifest in module initializer? 3. Maybe the app does not want to use these features (eg high DPI). 4. Maybe not possible to use correct manifest, eg when used in a scripting app. 5. Etc. // Never mind. The required manifest is documented. /// /// Floating toolbar. /// Can be attached to windows of other programs. /// /// /// To create toolbar code can be used menu TT > New toolbar. /// /// Not thread-safe. All functions must be called from the same thread that created the object, except where documented otherwise. Note: item actions by default run in other threads; see . /// public partial class toolbar : MTBase { record class _Settings : JSettings { public static _Settings Load(string file, bool useDefault) => Load<_Settings>(file, useDefault); public TBAnchor anchor = TBAnchor.TopLeft; public TBLayout layout; public TBBorder border = TBBorder.Width2; public bool dispText = true, sizable = true, autoSize = true; public TBFlags miscFlags = TBFlags.HideWhenFullScreen | TBFlags.ActivateOwnerWindow; public System.Windows.Size size = new(150, 24); public double wrapWidth; public TBOffsets offsets; // = new(150, 5, 7, 7); public int screenx, screeny; } readonly _Settings _sett; readonly List _a = new(); bool _created; bool _closed; bool _topmost; //no owner, or owner is topmost double _dpiF; //DPI scaling factor, eg 1.5 for 144 dpi static int s_treadId; /// /// Toolbar name. Must be valid filename. /// Used for: toolbar window name, settings file name, , some other functions. /// If null, uses the caller function's name if available, else exception. /// /// /// [](xref:caller_info) /// [](xref:caller_info) /// [](xref:caller_info) /// null or full path of the settings file of this toolbar. /// Invalid name. /// /// Each toolbar has a settings file, where are saved its position, size and context menu settings. This function reads the file if exists, ie if settings changed in the past. See . If fails, prints a warning and uses default settings. /// /// Sets properties: /// - = true. /// - = true. /// public toolbar(string name = null, TBCtor flags = 0, [CallerFilePath] string f_ = null, [CallerLineNumber] int l_ = 0, [CallerMemberName] string m_ = null, string settingsFile = null) : base(name, f_, l_, m_) { if (s_treadId == 0) s_treadId = _threadId; else if (_threadId != s_treadId) print.warning("All toolbars should be in single thread. Multiple threads use more CPU. If using triggers, insert this code before adding toolbar triggers: Triggers.Options.ThreadOfTriggers(); or Triggers.Options.ThreadThis();"); settingsFile = flags.Has(TBCtor.DontSaveSettings) ? null : (settingsFile ?? getSettingsFilePath(_name)); _sett = _Settings.Load(settingsFile, flags.Has(TBCtor.ResetSettings)); _offsets = _sett.offsets; //TODO3: don't use saved offsets if this toolbar was free and now is owned or vice versa. Because the position is irrelevent and may be far/offscreen. It usually happens when testing. ActionThread = true; ExtractIconPathFromCode = true; } /// /// Gets the toolbar window. /// public wnd Hwnd => _w; /// /// Returns true if the toolbar is open. False if closed or still not called. /// public bool IsOpen => _created && !_closed; /// /// Gets the name of the toolbar. /// public string Name => _name; /// public override string ToString() => _IsSatellite ? " " + Name : Name; //the indentation is for the list in the Active toolbars dialog /// /// True if this toolbar started with default settings. False if loaded saved settings from file. /// /// /// public bool FirstTime => !_sett.LoadedFile; #region static functions /// /// Gets full path of toolbar's settings file. The file may exist or not. /// /// Toolbar name. If this string is a full path, returns this string. /// /// Path: folders.Workspace + $@"\.toolbars\{toolbarName}.json". If is null, uses . /// public static string getSettingsFilePath(string toolbarName) { if (toolbarName.NE()) throw new ArgumentException("Empty name"); if (pathname.isFullPath(toolbarName)) return toolbarName; string s = folders.Workspace.Path; if (s != null) return s + @"\.toolbars\" + toolbarName + ".json"; s = s_settingsDir; if (s == null) { s = folders.ThisAppDataRoaming + ".toolbars"; if (!filesystem.exists(s).Directory) { //fbc. Previously was folders.ThisAppDocuments, but a library should not create directories there. bool nac = folders.noAutoCreate; folders.noAutoCreate = true; var s2 = folders.ThisAppDocuments + ".toolbars"; folders.noAutoCreate = nac; if (filesystem.exists(s2).Directory) s = s2; } s_settingsDir = s += @"\"; } return s + toolbarName + ".json"; } static string s_settingsDir; /// /// Finds an open toolbar by . /// /// null if not found or closed or never shown ( not called). /// /// Finds only toolbars created in the same script and thread. /// /// Does not find satellite toolbars. Use this code: toolbar.find("owner toolbar").Satellite /// public static toolbar find(string name) => _Manager._atb.Find(o => o.Name == name); internal static void TriggerActionEndedInToolbarUnfriendlyThread_() { if (_Manager._atb.Count > 0) print.warning("Toolbars in wrong thread. Insert this code before adding toolbar triggers: Triggers.Options.ThreadOfTriggers(); or Triggers.Options.ThreadThis();", -1); } #endregion #region add item void _Add(TBItem item, string text, Delegate click, MTImage image, int l_, string f_) { _ThreadTrap(); _CreatedTrap(_closed ? null : "cannot add items while the toolbar is open. To add to submenu, use the submenu variable."); item.Set_(this, text, click, image, l_, _sourceFile == null ? null : f_); _a.Add(item); Last = item; } /// /// Adds button. /// Same as . /// /// Text. Or "Text|Tooltip", or "|Tooltip", or "Text|". Separator can be "|" or "\0 " (then "|" isn't a separator). To always display text regardless of , append "\a", like "Text\a" or "Text\a|Tooltip". /// Action called when the button clicked. /// Image. Read here: . /// [](xref:caller_info) /// [](xref:caller_info) /// /// More properties can be specified later (set properties of the returned or use ) or before (, , , ). /// public TBItem Add(string text, Action click, MTImage image = default, [CallerLineNumber] int l_ = 0, [CallerFilePath] string f_ = null) { var item = new TBItem(this, TBItemType.Button); _Add(item, text, click, image, l_, f_); return item; } /// /// Adds button. /// Same as . /// /// Action called when the button clicked. /// /// More properties can be specified later (set properties of or use ) or before (, , , ). /// /// /// These two are the same. /// print.it(o)); /// tb["Example"] = o => print.it(o); /// ]]> /// These four are the same. /// print.it(o)); b.Tooltip="tt"; /// tb.Add("Example", o => print.it(o)).Tooltip="tt"; /// tb["Example"] = o => print.it(o); var b=tb.Last; b.Tooltip="tt"; /// tb["Example"] = o => print.it(o); tb.Last.Tooltip="tt"; /// ]]> /// /// public Action this[string text, MTImage image = default, [CallerLineNumber] int l_ = 0, [CallerFilePath] string f_ = null] { set { Add(text, value, image, l_, f_); } } //CONSIDER: AddCheck, AddRadio. /// /// Adds button with drop-down menu. /// /// Action that adds menu items. Called whenever the button clicked. /// /// The submenu is a object. It inherits these properties of this toolbar: , , , . /// /// /// { /// m["M1"]=o=>print.it(o); /// m["M2"]=o=>print.it(o); /// }); /// ]]> /// /// public TBItem Menu(string text, Action menu, MTImage image = default, [CallerLineNumber] int l_ = 0, [CallerFilePath] string f_ = null) { var item = new TBItem(this, TBItemType.Menu); _Add(item, text, menu, image, l_, f_); return item; } /// /// Adds button with drop-down menu. /// /// Func that returns the menu. Called whenever the button clicked. /// /// The caller creates the menu (creates the object and adds items) and can reuse it many times. Other overload does not allow to create and reuse same object. /// The submenu does not inherit properties of this toolbar. /// /// /// m); /// ]]> /// /// public TBItem Menu(string text, Func menu, MTImage image = default, [CallerLineNumber] int l_ = 0, [CallerFilePath] string f_ = null) { var item = new TBItem(this, TBItemType.Menu); _Add(item, text, menu, image, l_, f_); return item; } /// /// Adds new vertical separator. Horizontal if vertical toolbar. /// public TBItem Separator() { int i = _a.Count - 1; if (i < 0 || _a[i].IsGroup_) throw new InvalidOperationException("first item is separator"); var item = new TBItem(this, TBItemType.Separator); _Add(item, null, null, default, 0, null); return item; } /// /// Adds new horizontal separator, optionally with text. /// /// Text. Or "Text|Tooltip", or "|Tooltip", or "Text|". Separator can be "|" or "\0 " (then "|" isn't a separator). public TBItem Group(string text = null) { var item = new TBItem(this, TBItemType.Group); _Add(item, text, null, default, 0, null); return item; } /// /// Gets the last added item. /// public TBItem Last { get; private set; } /// /// Gets added buttons. /// /// /// Allows to set properties of multiple buttons in single place instead of after each "add button" code line. /// Skips separators and groups. /// public IEnumerable Items { get { _ThreadTrap(); foreach (var v in _a) { if (!v.IsSeparatorOrGroup_) yield return v; } } } #endregion #region show, close, owner /// /// Shows the toolbar. /// /// /// Attach to this screen. For example a screen index (0 the primary, 1 the first non-primary, and so on). Example: screen.index(1). /// If not specified, the toolbar will be attached to the screen where it is now or where will be moved later. /// Don't use this parameter if this toolbar was created by , because then screen is already known. /// /// The toolbar was created by , and now screen specified again. /// Show already called. /// /// The toolbar will be moved when the screen moved or resized. /// public void Show(screen screen = default) { if (!_screenAHSE.IsEmpty) { if (screen.IsEmpty) screen = _screenAHSE; else throw new ArgumentException(); } _Show(false, default, null, screen); } /// /// Shows the toolbar and attaches to a window. /// /// Window or control. Can belong to any process. /// Let the toolbar position be relative to the client area of the window. /// Show already called. /// ownerWindow is 0. /// /// The toolbar will be above the window in the Z order; moved when the window moved or resized; hidden when the window hidden, cloaked or minimized; destroyed when the window destroyed. /// public void Show(wnd ownerWindow, bool clientArea = false) { _followClientArea = clientArea; _Show(true, ownerWindow, null, _screenAHSE); } /// /// Shows the toolbar and attaches to an object in a window. /// /// Window that contains the object. Can be control. Can belong to any process. /// A variable of a user-defined class that implements interface. It provides object location, visibility, etc. /// Show already called. /// ownerWindow is 0. /// /// The toolbar will be above the window in the Z order; moved when the object or window moved or resized; hidden when the object or window hidden, cloaked or minimized; destroyed when the object or window destroyed. /// public void Show(wnd ownerWindow, ITBOwnerObject oo) => _Show(true, ownerWindow, oo, default); /// /// Shows the toolbar. /// If ta is , attaches the toolbar to the trigger window. /// Else if ta != null, calls . /// public void Show(TriggerArgs ta) { if (ta is WindowTriggerArgs wta) { Show(wta.Window); } else { Show(); ta?.DisableTriggerUntilClosed(this); } } //used for normal toolbars, not for satellite toolbars void _Show(bool owned, wnd owner, ITBOwnerObject oo, screen screen) { _ThreadTrap(); _CreatedTrap("this toolbar is already open"); wnd c = default; if (owned) { if (owner.Is0) throw new ArgumentException(); var w = owner.Window; if (w.Is0) return; if (w != owner) { c = owner; owner = w; } if (!_screenAHSE.IsEmpty) _sett.anchor |= TBAnchor.Screen; } _CreateWindow(owned, owner, screen); _Manager.Add(this, owner, c, oo); } //used for normal and satellite toolbars void _CreateWindow(bool owned, wnd owner, screen screen = default, bool isSatelite = false) { _topmost = !owned || owner.IsTopmost; if (!owned || Anchor.OfScreen()) _os = new _OwnerScreen(this, screen); else screen = screen.of(owner); _RegisterWinclass(); if (_os != null) _SetDpi(); else _SetDpi(screen.Dpi); //OwnerWindow still not set _Images(false); _MeasureText(); var size = _Measure(); var style = WS.POPUP | WS.CLIPCHILDREN | _BorderStyle(_sett.border); var estyle = WSE.TOOLWINDOW | WSE.NOACTIVATE; if (_topmost) estyle |= WSE.TOPMOST; var r = screen.Rect; //create in center of screen, to minimize possibility of DPI change when setting final position r = new(r.CenterX - size.width / 2, r.CenterY - size.height / 2, size.width, size.height); Dpi.AdjustWindowRectEx(_dpi, ref r, style, estyle); WndUtil.CreateWindow(_WndProc, true, "Au.toolbar", _name, style, estyle, r.left, r.top, r.Width, r.Height, isSatelite ? owner : default); _created = true; } /// /// Destroys the toolbar window. /// /// /// Can be called from any thread. /// Does nothing if not open. /// public void Close() { if (_w.Is0) return; if (_IsOtherThread) _w.Post(Api.WM_CLOSE); else Api.DestroyWindow(_w); if (_hasCachedImages) _ImageCache.Cleared -= _UpdateCachedImages; } /// /// When the toolbar window destroyed. /// public event Action Closed; private protected override void _WmNcdestroy() { _closed = true; _Manager.Remove(this); _SatDestroying(); _sett.Dispose(); base._WmNcdestroy(); Closed?.Invoke(); } /// /// Adds or removes a reason to temporarily hide the toolbar. The toolbar is hidden if at least one reason exists. See also . /// /// true to hide (add reason), false to show (remove reason). /// A user-defined reason to hide/unhide. Can be or a bigger value, eg (TBHide)0x20000, (TBHide)0x40000. /// /// - The toolbar was never shown ( not called). /// - It is a satellite toolbar. /// - Wrong thread. Must be called from the same thread that created the toolbar. See . /// /// reason is less than . /// /// Toolbars are automatically hidden when the owner window is hidden, minimized, etc. This function can be used to hide toolbars for other reasons. /// public void Hide(bool hide, TBHide reason) { _ThreadTrap(); if (!_created || _IsSatellite) throw new InvalidOperationException(); if (0 != ((int)reason & 0xffff)) throw new ArgumentOutOfRangeException(); _SetVisible(!hide, reason); } /// /// Gets current reasons why the toolbar is hidden. Returns 0 if not hidden. /// /// /// Not used with satellite toolbars. /// public TBHide Hidden => _hide; void _SetVisible(bool show, TBHide reason) { //print.it(show, reason); if (show) { if (_hide == 0) return; _hide &= ~reason; if (_hide != 0) return; } else { var h = _hide; _hide |= reason; if (h != 0) return; } _SetVisibleL(show); } TBHide _hide; void _SetVisibleL(bool show) => _w.ShowL(show); /// /// Returns true if the toolbar is attached to a window or an object in a window. /// public bool IsOwned => _ow != null; /// /// Returns the owner top-level window. /// Returns default(wnd) if the toolbar is not owned. See . /// public wnd OwnerWindow => _ow?.w ?? default; #endregion #region wndproc, context menu static void _RegisterWinclass() { if (0 == Interlocked.Exchange(ref s_winclassRegistered, 1)) { WndUtil.RegisterWindowClass("Au.toolbar"/*, etc: new() { style = Api.CS_HREDRAW | Api.CS_VREDRAW, mCursor = MCursor.Arrow }*/); } } static int s_winclassRegistered; unsafe nint _WndProc(wnd w, int msg, nint wParam, nint lParam) { //WndUtil.PrintMsg(w, msg, wParam, lParam); bool activatedOwner = false; switch (msg) { case Api.WM_LBUTTONDOWN or Api.WM_RBUTTONDOWN or Api.WM_MBUTTONDOWN: var tb1 = _SatPlanetOrThis; if (tb1.IsOwned && tb1.MiscFlags.Has(TBFlags.ActivateOwnerWindow)) { var wo = tb1.OwnerWindow; if (!wo.IsActive) activatedOwner = wo.ActivateL(); } break; case Api.WM_MOUSEMOVE or Api.WM_NCMOUSEMOVE: _SatMouse(); break; } switch (msg) { case Api.WM_NCCREATE: _WmNccreate(w); if (_transparency != default) _w.SetTransparency(true, _transparency.opacity, _transparency.colorKey); break; case Api.WM_NCDESTROY: _WmNcdestroy(); //PROBLEM: not called if thread ends without closing the toolbar window. // Then saves settings only on process exit. Not if process terminated. // In most cases it isn't a problem because saves settings every 2 s (IIRC). break; case Api.WM_ERASEBKGND: return 0; case Api.WM_PAINT: using (BufferedPaint bp = new(w, true)) { var dc = bp.DC; using var g = System.Drawing.Graphics.FromHdc(dc); g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; _WmPaint(dc, g, bp.Rect, bp.UpdateRect); } return 0; case Api.WM_MOUSEACTIVATE: return Api.MA_NOACTIVATE; case Api.WM_MOUSEMOVE: _WmMousemove(lParam); return 0; case Api.WM_MOUSELEAVE: _WmMouseleave(); return 0; case Api.WM_LBUTTONDOWN: _WmMouselbuttondown(lParam, activatedOwner); return 0; case Api.WM_CONTEXTMENU or Api.WM_NCRBUTTONUP: _WmContextmenu(); break; case Api.WM_NCHITTEST: if (_WmNchittest(lParam, out int hitTest)) return hitTest; //returns a hittest code to move or resize if need break; case Api.WM_NCLBUTTONDOWN: int ht = (int)wParam; if (ht == Api.HTCAPTION || (ht >= Api.HTSIZEFIRST && ht <= Api.HTSIZELAST)) { //workaround for: Windows tries to activate this window when moving or sizing it, unless this process is not allowed to activate windows. // This window then may not become the foreground window, but it receives wm_activateapp, wm_activate, wm_setfocus, and is moved to the top of Z order. // tested: LockSetForegroundWindow does not work. // This code better would be under WM_SYSCOMMAND, but then works only when sizing. When moving, activates before WM_SYSCOMMAND. using (WindowsHook.ThreadCbt(d => d.code == HookData.CbtEvent.ACTIVATE)) { //also let arrow keys move/resize by 1 pixel. // In active window Ctrl+arrow work automatically, but toolbars aren't active. // Never mind: does not work if active window has higher UAC IL. Even with Ctrl. using var k1 = new RegisteredHotkey(); k1.Register(100, KKey.Left, _w); using var k2 = new RegisteredHotkey(); k2.Register(101, KKey.Right, _w); using var k3 = new RegisteredHotkey(); k3.Register(102, KKey.Up, _w); using var k4 = new RegisteredHotkey(); k4.Register(103, KKey.Down, _w); Api.DefWindowProc(w, msg, wParam, lParam); } return 0; } break; case Api.WM_ENTERSIZEMOVE: _SetInMoveSize(true); break; case Api.WM_EXITSIZEMOVE: _SetInMoveSize(false); break; case Api.WM_WINDOWPOSCHANGING: _WmWindowPosChanging(ref *(Api.WINDOWPOS*)lParam); break; case RegisteredHotkey.WM_HOTKEY: int hkid = (int)wParam - 100; if (hkid >= 0 && hkid <= 3) { POINT p = mouse.xy; Api.SetCursorPos(p.x + hkid switch { 0 => -1, 1 => 1, _ => 0 }, p.y + hkid switch { 2 => -1, 3 => 1, _ => 0 }); return 0; } break; case Api.WM_GETOBJECT: if (_WmGetobject(wParam, lParam, out var r1)) return r1; break; case Api.WM_USER + 50: //posted by acc dodefaultaction if (!_closed) { int i = (int)wParam; if ((uint)i < _a.Count) _Click(i, false); } return 0; case Api.WM_USER + 51: _toolbarsDialog(); [MethodImpl(MethodImplOptions.NoInlining)] //don't load System.Windows.Forms assembly static void _toolbarsDialog() => toolbarsDialog(); return 0; } var R = Api.DefWindowProc(w, msg, wParam, lParam); switch (msg) { case Api.WM_WINDOWPOSCHANGED: _WmWindowPosChanged(in *(Api.WINDOWPOS*)lParam); break; case Api.WM_GETDPISCALEDSIZE: return _WmGetDpiScaledSize(wParam, lParam); case Api.WM_DPICHANGED: _WmDpiChanged(wParam, lParam); break; case Api.WM_DISPLAYCHANGE: _WmDisplayChange(); break; //case Api.WM_SETTINGCHANGE: // _WmSettingChange(wParam, lParam); // break; } return R; } #endregion #region input, context menu int _HitTest(POINT p) { for (int i = 0; i < _a.Count; i++) { if (_a[i].rect.Contains(p)) return i; } return -1; } unsafe void _WmMousemove(nint lParam) { if (_iClick < 0) { var p = Math2.NintToPOINT(lParam); int i = _HitTest(p); if (i != _iHot) { if (_iHot >= 0) _Invalidate(_iHot); if ((_iHot = i) >= 0) { _Invalidate(_iHot); var b = _a[i]; _SetTooltip(b, b.rect, lParam); } } if (_iHot >= 0 != _trackMouseEvent) _trackMouseEvent = Api.TrackMouseLeave(_w, _iHot >= 0) && _iHot >= 0; } } int _iHot = -1, _iClick = -1; bool _trackMouseEvent, _noHotClick; void _WmMouseleave() { _trackMouseEvent = false; if (_iHot >= 0) { _Invalidate(_iHot); _iHot = -1; } } void _WmMouselbuttondown(nint lParam, bool activatedOwner) { var mod = keys.gui.getMod(); if (mod == 0) { //click button var p = Math2.NintToPOINT(lParam); int i = _HitTest(p); if (i < 0) return; if (activatedOwner && _a[i].IsMenu_) { //let manager process OBJECT_REORDER. And let owner process WM_ACTIVATE. Else the menu sometimes disappears. timer.after(50, _ => { if (IsOpen) _Click(i, true); }); } else { _Click(i, true); } //} else if(mod==KMod.Shift) { //move toolbar // var p=mouse.xy; // DragDropUtil.SimpleDragDrop(_w, MButtons.Left, d => { // if (d.Msg.message != Api.WM_MOUSEMOVE) return; // var v=mouse.xy; if(v==p) return; // int dx=v.x-p.x, dy=v.y-p.y; // p=v; // var r=_w.Rect; // _w.MoveL(r.left+dx, r.top+dy); // }); } } int _menuClosedIndex; long _menuClosedTime; void _Click(int i, bool real) { var b = _a[i]; if (b.clicked == null) return; if (b.IsMenu_) { if (i == _menuClosedIndex && perf.ms - _menuClosedTime < 100) return; popupMenu m = null; if (b.clicked is Action menu) { m = new popupMenu(null, NoContextMenu.Has(TBNoMenu.Edit) ? null : b.sourceFile, b.sourceLine); _CopyProps(m); menu(m); } else if (b.clicked is Func func) { m = func(); } if (m == null) return; var r = b.rect; _w.MapClientToScreen(ref r); m.Show(PMFlags.AlignRectBottomTop, new(r.left, r.bottom), r, owner: _w); _menuClosedIndex = i; _menuClosedTime = perf.ms; //info: before wm_lbuttondown the menu is already closed and its message loop ended. Previous Show returns before new Show. } else { if (real) { bool ok = false; try { _Invalidate(_iClick = i); ok = WndUtil.DragLoop(_w, MButtons.Left, d => { if (d.msg.message != Api.WM_MOUSEMOVE) return; int j = _HitTest(Math2.NintToPOINT(d.msg.lParam)); if ((j == i) == _noHotClick) { _noHotClick ^= true; _Invalidate(i); } }) && !_noHotClick; } finally { _iClick = -1; _noHotClick = false; _Invalidate(i); } if (!ok) return; } //print.it("click", b); if (b.actionThread) run.thread(() => _ExecItem(), background: false); //thread start speed: 250 mcs else _ExecItem(); void _ExecItem() { var action = b.clicked as Action; try { action(b); } catch (Exception ex) when (!b.actionException) { print.warning(ex); } } } } void _WmContextmenu() { var no = NoContextMenu; if (no.Has(TBNoMenu.Menu)) return; if (_a.Count == 0 && _satellite != null) no |= TBNoMenu.Layout | TBNoMenu.AutoSize | TBNoMenu.Text; var p = _w.MouseClientXY; int i = _HitTest(p); var item = i < 0 ? null : _a[i]; var m = new popupMenu(); if (!no.Has(TBNoMenu.Edit | TBNoMenu.File)) { string sf; int sl; if (item != null) { sf = item.sourceFile; sl = item.sourceLine; } else { sf = _sourceFile; sl = _sourceLine; } var (canEdit, canGo, goText) = MTItem.CanEditOrGoToFile_(sf, item); if (!no.Has(TBNoMenu.Edit) && canEdit) m["Edit toolbar"] = o => ScriptEditor.Open(sf, sl); if (!no.Has(TBNoMenu.File) && canGo) m[goText] = o => item.GoToFile_(); } if (!no.Has(TBNoMenu.Close)) m.Add("Close", o => _SatPlanetOrThis.Close()); if (m.Last != null) m.Separator(); if (!no.Has(TBNoMenu.Anchor)) { m.Submenu("Anchor", m => { _AddAnchor(TBAnchor.TopLeft); _AddAnchor(TBAnchor.TopRight); _AddAnchor(TBAnchor.BottomLeft); _AddAnchor(TBAnchor.BottomRight); _AddAnchor(TBAnchor.TopLR); _AddAnchor(TBAnchor.BottomLR); _AddAnchor(TBAnchor.LeftTB); _AddAnchor(TBAnchor.RightTB); _AddAnchor(TBAnchor.All); m.Separator(); _AddAnchor(TBAnchor.OppositeEdgeX); _AddAnchor(TBAnchor.OppositeEdgeY); if (IsOwned) _AddAnchor(TBAnchor.Screen); void _AddAnchor(TBAnchor an) { var k = an <= TBAnchor.All ? m.AddRadio(an.ToString(), Anchor.WithoutFlags() == an, _ => Anchor = (Anchor & ~TBAnchor.All) | an) : m.AddCheck(an.ToString(), Anchor.Has(an), _ => Anchor ^= an, disable: _GetInvalidAnchorFlags(Anchor).Has(an)); if (_IsSatellite) k.Tooltip = "Note: You may want to set anchor of the owner toolbar instead. Anchor of this auto-hide toolbar is relative to the owner toolbar."; } }); } if (!no.Has(TBNoMenu.Layout)) { m.Submenu("Layout", m => { _AddLayout(TBLayout.HorizontalWrap); _AddLayout(TBLayout.Vertical); //_AddLayout(TBLayout.Horizontal); void _AddLayout(TBLayout tl) { m.AddRadio(tl.ToString(), tl == Layout, _ => Layout = tl); } }); } if (!no.Has(TBNoMenu.Border)) { m.Submenu("Border", m => { _AddBorder(TBBorder.None); _AddBorder(TBBorder.Width1); _AddBorder(TBBorder.Width2); _AddBorder(TBBorder.Width3); _AddBorder(TBBorder.Width4); _AddBorder(TBBorder.ThreeD); _AddBorder(TBBorder.Thick); _AddBorder(TBBorder.Caption); _AddBorder(TBBorder.CaptionX); void _AddBorder(TBBorder b) { m.AddRadio(b.ToString(), b == Border, _ => Border = b); } }); } if (!no.Has(TBNoMenu.Sizable | TBNoMenu.AutoSize | TBNoMenu.Text | TBNoMenu.MiscFlags)) { m.Submenu("More", m => { if (!no.Has(TBNoMenu.Sizable)) m.AddCheck("Sizable", Sizable, _ => Sizable ^= true); if (!no.Has(TBNoMenu.AutoSize)) m.AddCheck("Auto-size", AutoSize, _ => AutoSize ^= true); if (!no.Has(TBNoMenu.Text)) m.AddCheck("Display text", DisplayText, _ => DisplayText ^= true); if (!no.Has(TBNoMenu.MiscFlags)) { _AddFlag(TBFlags.HideWhenFullScreen); if (_SatPlanetOrThis.IsOwned) _AddFlag(TBFlags.ActivateOwnerWindow); void _AddFlag(TBFlags f) { var tb = _SatPlanetOrThis; m.AddCheck(_EnumToString(f), tb.MiscFlags.Has(f), _ => tb.MiscFlags ^= f); } static string _EnumToString(Enum e) { var s = e.ToString().RxReplace(@"(?<=[^A-Z])[A-Z]", m => " " + m.Value.Lower()); //s = s.Replace("Dont", "Don't"); return s; } } }); } if (!no.Has(TBNoMenu.Toolbars | TBNoMenu.Help) && m.Last != null && !m.Last.IsSeparator) m.Separator(); if (!no.Has(TBNoMenu.Toolbars)) m.Add("Toolbars", o => toolbarsDialog()); if (!no.Has(TBNoMenu.Help)) m["How to"] = _ => dialog.showInfo("How to", @"Move toolbar: Shift+drag. Resize toolbar: drag border. Cannot resize if in context menu is unchecked or unavailable More > Sizable; or if checked Border > None. Move or resize precisely: start to move or resize but don't move the mouse. Instead release Shift and press arrow keys. Finally release the mouse button. "); if (m.Last != null) m.Show(); } bool _WmNchittest(nint xy, out int ht) { ht = 0; if (keys.gui.getMod() == KMod.Shift) { //move ht = Api.HTCAPTION; } else { //resize? if (Border == TBBorder.None || (!Sizable && Border < TBBorder.Thick)) return false; var (x, y) = Math2.NintToPOINT(xy); if (Sizable) { _w.GetWindowInfo_(out var k); RECT r = k.rcWindow; int b = Border >= TBBorder.ThreeD ? k.cxWindowBorders : (_a.Count > 0 ? _BorderPadding() : Dpi.Scale(6, _dpi)); //make bigger if no buttons. Eg if auto-hide-at-screen-edge, border 1 is difficult to resize. int bx = Math.Min(b, r.Width / 2), by = Math.Min(b, r.Height / 2); if (bx == 0) bx = 1; if (by == 0) by = 1; //print.it(bx, by); int x1 = r.left + bx, x2 = --r.right - bx, y1 = r.top + by, y2 = --r.bottom - by; if (r.Width > bx * 8 && r.Height > by * 8) { //if toolbar isn't small, in corners allow to resize both width and height at the same time if (x < x1) ht = y < y1 ? Api.HTTOPLEFT : (y > y2 ? Api.HTBOTTOMLEFT : Api.HTLEFT); else if (x > x2) ht = y < y1 ? Api.HTTOPRIGHT : (y > y2 ? Api.HTBOTTOMRIGHT : Api.HTRIGHT); else if (y < y1) ht = Api.HTTOP; else if (y > y2) ht = Api.HTBOTTOM; else return false; } else if (r.Width >= r.Height) { //in corners prefer width if (x < x1) ht = Api.HTLEFT; else if (x > x2) ht = Api.HTRIGHT; else if (y < y1) ht = Api.HTTOP; else if (y > y2) ht = Api.HTBOTTOM; else return false; } else { //in corners prefer height if (y < y1) ht = Api.HTTOP; else if (y > y2) ht = Api.HTBOTTOM; else if (x < x1) ht = Api.HTLEFT; else if (x > x2) ht = Api.HTRIGHT; else return false; } } else { //disable resizing if border is natively sizable if (Border < TBBorder.Thick) return false; _w.GetWindowInfo_(out var k); k.rcWindow.Inflate(-k.cxWindowBorders, -k.cyWindowBorders); if (k.rcWindow.Contains(x, y)) return false; ht = Api.HTBORDER; } } return true; } #endregion #region properties /// /// Whether to DPI-scale toolbar size and offsets. /// Default: scale size; scale offsets if anchor is not screen. /// /// /// The unit of measurement of , and some other properties depends on whether scaling is used for that property. If scaling is used, the unit is logical pixels; it is 1/96 inch regardless of screen DPI. If scaling not used, the unit is physical pixels. Screen DPI can be changed in Windows Settings; when it is 100%, logical and physical pixels are equal. /// public TBScaling DpiScaling { get; set; } /// /// Toolbar width and height without non-client area when false. /// /// /// Non-client area is border and title bar when is ThreeD, Thick, Caption or CaptionX. /// /// The unit of measurement depends on . /// /// This property is updated when resizing the toolbar. It is saved. /// /// /// /// public System.Windows.Size Size { get => _sett.size; set { _ThreadTrap(); if (value != _sett.size) { value = new(_Limit(value.Width), _Limit(value.Height)); _sett.size = value; if (IsOpen && !AutoSize) _Resize(_Scale(value)); } if (!_followedOnce) _preferSize = true; } } /// /// Whether the border can be used to resize the toolbar. /// Default true. /// /// /// This property is in the context menu and is saved. /// public bool Sizable { get => _sett.sizable; set => _sett.sizable = value; } /// /// Automatically resize the toolbar to make all buttons visible. /// Default true. /// /// /// This property is in the context menu and is saved. /// public bool AutoSize { get => _sett.autoSize; set { _ThreadTrap(); if (value != _sett.autoSize) { _sett.autoSize = value; _AutoSizeNowIfIsOpen(); } } } /// /// When is true, this is the preferred width at which buttons are moved to the next row. Unlimited if 0. /// /// /// The unit of measurement depends on . /// /// This property is updated when the user resizes the toolbar while is true. It is saved. /// /// If layout of this toolbar is vertical, just sets max width. /// public double AutoSizeWrapWidth { get => _sett.wrapWidth; set { _ThreadTrap(); value = value > 0 ? _Limit(value) : 0; if (value != _sett.wrapWidth) { _sett.wrapWidth = value; _AutoSizeNowIfIsOpen(); } } } /// /// Specifies to which owner's edges the toolbar keeps constant distance when moving or resizing the owner. /// /// /// The owner can be a window, screen, control or other object. It is specified when calling . /// This property is in the context menu and is saved. /// /// public TBAnchor Anchor { get => _sett.anchor; set { _ThreadTrap(); value &= ~_GetInvalidAnchorFlags(value); if (value.WithoutFlags() == 0) value |= TBAnchor.TopLeft; if (value == _sett.anchor) return; var prev = _sett.anchor; _sett.anchor = value; if (IsOwned) { _os = value.OfScreen() ? new _OwnerScreen(this, default) : null; //follow toolbar's screen if (prev.OfScreen() && _followedOnce) { if (_oc != null) _oc.UpdateRect(out _); else _ow.UpdateRect(out _); } } if (_followedOnce) { var r = _w.Rect; _UpdateOffsets(r.left, r.top, r.Width, r.Height); } } } /// /// Specifies distances between edges of the toolbar and edges of its owner, depending on . /// /// /// Owner is specified when calling . It can be a window, screen, control or other object. /// /// The type has 4 properties - Top, Bottom, Left and Right, but used are only those included in . For example, if Anchor is TopLeft, used are only Top and Left. /// /// The unit of measurement depends on and whether anchor is screen. /// /// This property is updated when moving or resizing the toolbar. It is saved. /// /// /// /// public TBOffsets Offsets { get => _offsets; set { _ThreadTrap(); _preferSize = false; if (value == _offsets) return; _sett.offsets = _offsets = value; if (_followedOnce) _FollowRect(); } } TBOffsets _offsets; /// /// Number of pixels to add to the top of the retrieved rectangle of the owner window when it is maximized. That is, move this toolbar slightly down (if positive) or up (if negative). /// /// /// When you create a toolbar attached to a window, and the anchor is at the top, test whether it is in visually the same vertical position (relative to UI elements of the window) when the window is maximized and when not maximized. On some windows it will be in a different position, and it is not what you want. Reason: some windows, when maximized, draw their UI elements in a different vertical position than when not maximized. It's impossible to reliably correct it automatically. To correct it, set this property before calling or . With most such windows, the good value is 6.7 or 7.5. With some windows may need a negative value. /// /// By default the value is logical pixels; that is, will be scaled depending on DPI. /// /// If the owner window has multiple attached toolbars and all they run in the same thread, set this property for one of them, not for all. It will adjust the vertical position of all them. /// /// With some windows can be used another way to correct the vertical position: call with clientArea: true. /// /// If neither way works, use (you'll need to create a class that implements ). For example, to move the toolbar to a different position when the window is in full-screen mode. /// public double MaximizedWindowTopPlus { get; set; } //rejected. Would be rarely used, unless default 0. Avoid default limitations like this. We have a dialog to find lost toolbars. //public int MaxDistanceFromOwner { get; set; } = int.MaxValue; //rejected //public bool HideTextIfSmall { get; set; } //like ribbon UI /// /// Miscellaneous options. /// /// /// This property is in the context menu (submenu More) and is saved. /// public TBFlags MiscFlags { get => _sett.miscFlags; set { _sett.miscFlags = value; } } /// /// Opacity and transparent color. /// /// /// /// /// public (int? opacity, ColorInt? colorKey) Transparency { get => _transparency; set { if (value != _transparency) { _transparency = value; if (_created) _w.SetTransparency(value != default, value.opacity, value.colorKey); } } } (int? opacity, ColorInt? colorKey) _transparency; /// /// Gets or sets flags to hide some context menu items or menu itself. /// public TBNoMenu NoContextMenu { get; set; } #endregion } ================================================ FILE: Au/GUI/trayIcon.cs ================================================ namespace Au { /// /// Shows tray icon. /// /// /// Wraps API Shell_NotifyIconW, NOTIFYICONDATAW. More info there. /// /// This thread must dispatch messages. /// /// Can be used by multiple threads (eg one thread adds tray icon and other thread later changes its tooltip). /// /// Creates a hidden window that receives tray icon events (click etc). /// public class trayIcon : IDisposable { readonly int _id; readonly bool _disposeOnExit; wnd _w; //rejected. Various problems, eg the program file cannot be moved. Unclear documentation. // Guid _guid; static trayIcon() { s_msgTaskbarCreated = WndUtil.RegisterMessage("TaskbarCreated", uacEnable: true); WndUtil.RegisterWindowClass("trayIcon"); } static int s_msgTaskbarCreated; /// /// Tray icon notification message. /// protected const int MsgNotify = Api.WM_USER + 145; /// An id that helps Windows to distinguish multiple tray icons added by same program. Use 0, 1, 2, ... or all 0. /// /// Remove tray icon when process exits (). /// Note: can't remove if the process terminated or called or API ExitProcess. /// public trayIcon(int id = 0, bool disposeOnExit = true) { _disposeOnExit = disposeOnExit; _id = Hash.Fnv1(script.name) + id; ///// A GUID that identifies the tray icon. //_guid=guid; } /// /// Removes tray icon and disposes other resources. /// public void Dispose() { Dispose(true); } /// protected virtual void Dispose(bool disposing) { _Delete(disposing: true); } void _Delete(bool disposing = false) { lock (this) { if (_visible) { Api.Shell_NotifyIcon(Api.NIM_DELETE, _NewND()); _visible = false; } if (disposing && !_w.Is0) { if (!Api.DestroyWindow(_w)) _w.Post(Api.WM_CLOSE); _w = default; } } } /// /// Gets or sets whether the tray icon is visible. /// /// Icon not set. public bool Visible { get => _visible; set { lock (this) { if (value != _visible) { if (value && _icon == null) throw new InvalidOperationException("Icon not set"); //but allow ShowNotification without icon. This is to prevent using slow code like new trayIcon() { Visible=true, Icon=... }. if (value) _Update(); else _Delete(); } } } } bool _visible; /// /// Gets or sets icon. /// /// /// To display nice icon at any DPI, the icon should be loaded with or API LoadIconMetric, either from a native resource in your app or from an .ico file, which should contain icons of sizes 16, 32 and also recommended 20, 24. /// public icon Icon { get => _icon; set { lock (this) { if (value != _icon) { _icon = value; if (_visible) _Update(icon: true); } } } } icon _icon; /// /// Gets or sets tooltip text. /// /// /// Max length of displayed tooltip text is 127. /// public string Tooltip { get => _tooltip; set { lock (this) { if (value != _tooltip) { _tooltip = value; if (_visible) _Update(tooltip: true); } } } } string _tooltip; Api.NOTIFYICONDATA _NewND(uint nifFlags = 0, bool setTT = false) { var d = new Api.NOTIFYICONDATA(_w, nifFlags) { uID = _id, uCallbackMessage = MsgNotify, uVersion = Api.NOTIFYICON_VERSION_4 }; //if (_guid!=default) { d.uFlags|=Api.NIF_GUID; d.guidItem=_guid; } if (!_customPopup) d.uFlags |= Api.NIF_SHOWTIP; if (setTT) { d.uFlags |= Api.NIF_TIP; d.szTip = _customPopup ? " " : _tooltip?.Limit(127); } return d; } bool _Update(bool icon = false, bool tooltip = false, _Notification n = null, bool taskbarCreated = false) { if (script.Exiting_) return false; lock (this) { if (_w.Is0) { if (_disposeOnExit) process.thisProcessExit += _ => _Delete(); _w = WndUtil.CreateWindow(WndProc, true, "trayIcon", script.name, WS.POPUP, WSE.NOACTIVATE); } if (taskbarCreated) _visible = false; if (!_visible) { icon = _icon != null; tooltip = !_tooltip.NE(); } var d = _NewND(Api.NIF_MESSAGE, setTT: tooltip || _customPopup); if (icon) { d.uFlags |= Api.NIF_ICON; d.hIcon = _icon; } if (n != null) { d.uFlags |= Api.NIF_INFO; if (n.flags.Has(TINFlags.Realtime)) d.uFlags |= Api.NIF_REALTIME; d.dwInfoFlags = (uint)(n.flags & ~TINFlags.Realtime); if (n.icon != null) { d.dwInfoFlags |= 0x24; d.hBalloonIcon = n.icon; } //user icon, large d.szInfoTitle = n.title?.Limit(63); d.szInfo = n.text?.Limit(255); //if(!_visible) if(_icon==null && _tooltip.NE()) { d.uFlags|=Api.NIF_STATE; d.dwState=d.dwStateMask=1; } //hidden icon. But then no notification. } int nim = _visible ? Api.NIM_MODIFY : Api.NIM_ADD; bool ok = Api.Shell_NotifyIcon(nim, d); if (!ok && 4 == (d.dwInfoFlags & 15)) { //fails if size of custom icon of notification != XM_CXSMICON (if no NIIF_LARGE_ICON) or < SM_CXICON (if NIIF_LARGE_ICON). // tested: Even if now taskbar DPI is different (changed since this process started), need icon of these sizes (they didn't change since this process started). // Workaround: Remove the user icon flag and print warning. //tested: Win7 displays XM_CXSMICON or SM_CXICON depending on NIIF_LARGE_ICON. Win8.1 too. Win10 displays SM_CXICON*1.5 regardless of NIIF_LARGE_ICON. d.dwInfoFlags &= ~15u; ok = Api.Shell_NotifyIcon(nim, d); if (ok) print.warning("Custom icon size must be >= trayIcon.notificationIconSize"); } if (!_visible) { if (ok || taskbarCreated) ok = Api.Shell_NotifyIcon(Api.NIM_SETVERSION, d); _visible = true; //note: Shell_NotifyIcon(NIM_ADD) fails if already added, eg when "TaskbarCreated" message received when taskbar DPI changed. //note: Win10 sets last error, but Win7 doesn't. } //if(!ok) print.it("Shell_NotifyIcon: ", lastError.message); return ok; } } /// /// Shows temporary notification window by the tray icon. /// /// Title, max 63 characters. /// Text, max 255 characters. /// Standard icon and other flags. /// Custom icon. Important: use icon of size returned by . /// /// If the tray icon isn't visible, makes it visible. /// /// No more than one notification at a time can be displayed. If an application attempts to display a notification when one is already being displayed, the new notification is queued and displayed when the older notification goes away. /// /// Users may choose to not show notifications, depending on various conditions. Look in Windows Settings > System > Notifications. /// public void ShowNotification(string title, string text, TINFlags flags = 0, icon icon = default) { if (!_Update(n: new(title, text, flags, icon))) print.warning("ShowNotification() failed. " + lastError.message); } record class _Notification(string title, string text, TINFlags flags, icon icon); /// /// Hides notification. /// public void HideNotification() { lock (this) { if (_visible) Api.Shell_NotifyIcon(Api.NIM_MODIFY, _NewND(Api.NIF_INFO)); } } /// /// Gets icon size for . /// public static int notificationIconSize { get { int r = Api.GetSystemMetrics(Api.SM_CXICON); if (osVersion.minWin10) r = r * 3 / 2; return r; } } /// /// Activates taskbar and makes the tray icon focused for keyboard. /// /// /// If the tray icon is in hidden overflow area, makes the area button focused. /// public void Focus() { lock (this) { Api.Shell_NotifyIcon(Api.NIM_SETFOCUS, _NewND()); } } /// /// Together with identifies this tray icon. /// protected int Id => _id; /// /// A hidden window automatically created for this tray icon to receive its notifications. /// protected internal wnd Hwnd => _w; /// /// Window procedure of the hidden window that receives tray icon notifications () in version 4 format. /// If you override it, call the base function. /// protected virtual nint WndProc(wnd w, int msg, nint wParam, nint lParam) { if (_visible) { if (msg == MsgNotify) { int m = Math2.LoWord(lParam); POINT p = Math2.NintToPOINT(wParam); //if(m!=Api.WM_MOUSEMOVE) print.it(m, p); Message?.Invoke(new(m, p)); switch (m) { case Api.WM_CONTEXTMENU: RightClick?.Invoke(new(m, p)); break; case Api.NIN_SELECT: //mouse click or double click. After WM_LBUTTONUP. case Api.NIN_KEYSELECT: //Space, Enter (never mind bug: 2 NIN_KEYSELECT on Enter), DoDefaultAction Click?.Invoke(new(m, p)); break; case Api.WM_MBUTTONUP: MiddleClick?.Invoke(new(m, p)); break; case Api.NIN_BALLOONUSERCLICK: NotificationClick?.Invoke(new(m, p)); break; case Api.NIN_POPUPOPEN: //tested: on Win10 only if no NIF_SHOWTIP, but on Win7 always if (_customPopup) _popupOpen?.Invoke(new(m, p)); break; case Api.NIN_POPUPCLOSE: if (_customPopup) PopupClose?.Invoke(new(m, p)); break; } } else if (msg == s_msgTaskbarCreated) { //explorer restarted or taskbar DPI changed _Update(taskbarCreated: true); } } var R = Api.DefWindowProc(w, msg, wParam, lParam); if (msg == Api.WM_NCDESTROY) { _Delete(); } return R; } /// /// When received any message from the tray icon. /// /// /// Receives mouse messages, NIN_ messages and some other. See Shell_NotifyIconW. /// public event Action Message; /// /// When default action should be invoked (on click, Space/Enter, automation/accessibility API). /// /// /// If clicked, the parameter contains message NIN_SELECT (1024) and mouse coordinates. Else NIN_KEYSELECT (1025) and top-left of the tray icon. /// On double click there are two Click events. To distinguish click and double click events, use instead. /// public event Action Click; /// /// When a context menu should be shown (on right click or Apps key). /// public event Action RightClick; /// /// When the tray icon clicked with the middle button. /// [Obsolete("Does not work on Windows 11.")] public event Action MiddleClick; /// /// When clicked the notification window. /// public event Action NotificationClick; /// /// When it's time to close custom tooltip etc shown on . /// public event Action PopupClose; /// /// When it's time to open custom tooltip or some temporary popup window. /// If this event is used, does not show standard tooltip. /// public event Action PopupOpen { add => _SetPopupOpen(true, value); remove => _SetPopupOpen(false, value); } event Action _popupOpen; bool _customPopup; void _SetPopupOpen(bool add, Action a) { if (add) _popupOpen += a; else _popupOpen -= a; bool customPopup = _popupOpen != null; if (customPopup != _customPopup) { _customPopup = customPopup; if (_visible) Api.Shell_NotifyIcon(Api.NIM_MODIFY, _NewND(setTT: true)); } } /// /// Gets tray icon rectangle in screen. /// /// false if failed, for example if the icon is in hidden overflow area. Supports . public unsafe bool GetRect(out RECT r) { var x = new Api.NOTIFYICONIDENTIFIER { cbSize = sizeof(Api.NOTIFYICONIDENTIFIER), hWnd = _w, uID = _id }; //if (_guid!=default) x.guidItem=_guid; int hr = Api.Shell_NotifyIconGetRect(x, out r); if (hr == 0) return true; lastError.code = hr; return false; } // public bool GetPopupRect(int width, int height) { // if(!_visible) return false; // //what if hidden in overflow area? Then should not show popup. Then OS does not show notifications. // //CalculatePopupWindowPosition // } } } namespace Au.Types { /// /// Flags for . See NIIF_ flags of API NOTIFYICONDATAW. /// [Flags] public enum TINFlags { #pragma warning disable 1591 //no doc InfoIcon = 1, WarningIcon, ErrorIcon, //UserIcon, NoSound = 0x10, //LargeIcon=0x20, //RespectQuietTime=0x80, #pragma warning restore /// /// Flag NIF_REALTIME. /// Realtime = 0x10000000, } #pragma warning disable 1591 public record class TIEventArgs(int Message, POINT XY); #pragma warning restore } ================================================ FILE: Au/GUI/wpf-types.cs ================================================ using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Documents; using System.Windows.Media; namespace Au.Types { /// /// Used with constructor to specify the type of the root panel. /// public enum WBPanelType { /// Grid, /// Canvas, /// Dock, /// VerticalStack, /// HorizontalStack, } /// /// Flags for obsolete overloads. /// [Flags] [EditorBrowsable(EditorBrowsableState.Never)] public enum WBAdd { /// /// Add as child of , which can be of type (or base type): ///
. Adds as its property. For example you can add a CheckBox in a Button. ///
, for example . Adds as its property. ///
ChildOfLast = 1, /// /// Don't adjust properties of the element, except margin. By default it adjusts padding, alignment, etc, and properties specified in . /// DontSetProperties = 2, } /// /// Used with functions for width/height parameters. Allows to specify minimal and/or maximal values too. /// /// /// Has implicit conversions from double, and tuple (double length, Range minMax). /// To specify width or height, pass an int or double value, like 100 or 15.25. /// To specify minimal value, pass a range like 100... /// To specify maximal value, pass a range like ..100. /// To specify minimal and maximal values, pass a range like 100..500. /// To specify width or height and minimal or/and maximal values, pass a tuple like (100, 50..) or (100, ..200) or (100, 50..200). /// public struct WBLength { double _v; Range _r; WBLength(double v, Range r) { if (r.Start.IsFromEnd || (r.End.IsFromEnd && r.End.Value != 0)) throw new ArgumentException(); _v = v; _r = r; } /// public static implicit operator WBLength(double v) => new WBLength { _v = v, _r = .. }; /// public static implicit operator WBLength(Range v) => new WBLength(double.NaN, v); /// public static implicit operator WBLength((double length, Range minMax) v) => new WBLength(v.length, v.minMax); /// /// Gets the width or height value. Returns false if not set. /// public bool GetLength(out double value) { value = _v; return !double.IsNaN(_v); } /// /// Gets the minimal value. Returns false if not set. /// public bool GetMin(out int value) { value = _r.Start.Value; return value > 0; } /// /// Gets the maximal value. Returns false if not set. /// public bool GetMax(out int value) { value = _r.End.Value; return !_r.End.IsFromEnd; } /// /// Sets Width or Height or/and MinWidth/MinHeight or/and MaxWidth/MaxHeight of the . /// /// Element. /// Set Height. If false, sets Width. public void ApplyTo(FrameworkElement e, bool height) { if (GetLength(out double d)) { if (height) e.Height = d; else e.Width = d; } if (GetMin(out int i)) { if (height) e.MinHeight = i; else e.MinWidth = i; } if (GetMax(out i)) { if (height) e.MaxHeight = i; else e.MaxWidth = i; } } } /// /// Used with functions to specify width/height of columns and rows. Allows to specify minimal and/or maximal values too. /// /// /// Like , but has functions to create and . Also has implicit conversion from these types. /// public struct WBGridLength { double _v; Range _r; DefinitionBase _def; WBGridLength(double v, Range r) { if (r.Start.IsFromEnd || (r.End.IsFromEnd && r.End.Value != 0)) throw new ArgumentException(); _v = v; _r = r; _def = null; } /// public static implicit operator WBGridLength(double v) => new WBGridLength { _v = v, _r = .. }; /// public static implicit operator WBGridLength((double length, Range minMax) v) => new WBGridLength(v.length, v.minMax); /// public static implicit operator WBGridLength(Range v) => new WBGridLength(-1, v); /// public static implicit operator WBGridLength(DefinitionBase v) => new WBGridLength { _def = v }; /// /// Creates column definition object from assigned width or/and min/max width values. Or just returns the assigned or previously created object. /// public ColumnDefinition Column { get { if (_def is ColumnDefinition d) return d; d = new ColumnDefinition { Width = _GridLength(_v) }; if (_r.Start.Value > 0) d.MinWidth = _r.Start.Value; if (!_r.End.IsFromEnd) d.MaxWidth = _r.End.Value; _def = d; return d; } } /// /// Creates row definition object from assigned height or/and min/max height values. Or just returns the assigned or previously created object. /// public RowDefinition Row { get { if (_def is RowDefinition d) return d; d = new RowDefinition { Height = _GridLength(_v) }; if (_r.Start.Value > 0) d.MinHeight = _r.Start.Value; if (!_r.End.IsFromEnd) d.MaxHeight = _r.End.Value; _def = d; return d; } } GridLength _GridLength(double d) { if (d > 0) return new GridLength(d, GridUnitType.Pixel); if (d < 0) return new GridLength(-d, GridUnitType.Star); return new GridLength(); } } /// /// Arguments for callback function. /// public class WBAlsoAllArgs { /// /// Gets 0-based column index of last added control, or -1 if not in grid. /// public int Column { get; internal set; } /// /// Gets 0-based row index of last added control, or -1 if not in grid. /// public int Row { get; internal set; } } /// /// Arguments for callback function. /// public class WBButtonClickArgs : CancelEventArgs { /// /// Gets the button. /// public Button Button { get; internal set; } /// /// Gets the window. /// public Window Window { get; internal set; } } /// /// Flags for . /// [Flags] public enum WBBFlags { /// It is OK button (, closes window, validates, event). OK = 1, /// It is Cancel button (, closes window). Cancel = 2, /// It is Apply button (size like OK/Cancel, validates, event). Apply = 4, /// Perform validation like OK and Apply buttons. Validate = 8, } /// /// Obsolete. /// Can be used with to add a hyperlink. /// [EditorBrowsable(EditorBrowsableState.Never)] //obsolete public class WBLink { /// public Hyperlink Hlink { get; } WBLink(string text, bool bold) { Run run = new(text); Hlink = new(bold ? new Bold(run) : run); } /// /// Sets link text and action. /// public WBLink(string text, Action action, bool bold = false) : this(text, bold) { Hlink.Click += (o, e) => action(); } /// /// Sets link text and action. /// /// Link text. /// Action to execute on click. /// Bold font. public WBLink(string text, Action action, bool bold = false) : this(text, bold) { Hlink.Click += (o, e) => action(o as Hyperlink); } /// /// Sets link text and target URL or file etc. /// On click will call . /// /// Link text. /// URL or path for . If null, uses text. /// args for . /// Bold font. public WBLink(string text, string urlOrPath = null, string args = null, bool bold = false) : this(text, _ => run.itSafe(urlOrPath ?? text, args)) { } } } namespace Au.More { //rejected. Unsafe etc. For example, when assigning to object, uses CheckBool whereas the user may expect bool. // /// // /// that can be used like bool. // /// For example instead of if(c.IsChecked == true) can be used if(c). // /// // public class CheckBool : CheckBox // { // /// // public CheckBool() // { // this.SetResourceReference(StyleProperty, typeof(CheckBox)); // } // // /// // /// Returns true if == true. // /// // public static implicit operator bool(CheckBool c) => c.IsChecked.GetValueOrDefault(); // } /// /// Grid splitter control. Based on , changes its behavior. /// /// /// Try this class when does not work as you want. /// /// Limitations (bad or good): /// - Splitters must be on own rows/columns. Throws exception if ResizeBehavior is not PreviousAndNext (which is default). /// - Throws exception is there are star-sized splitter rows. /// - Does not resize auto-sized rows/columns. Only pixel-sized and star-sized. /// - With UseLayoutRounding may flicker when resizing, especially when high DPI. /// public class GridSplitter2 : GridSplitter { static GridSplitter2() { EventManager.RegisterClassHandler(typeof(GridSplitter2), Thumb.DragStartedEvent, new DragStartedEventHandler(_OnDragStarted)); EventManager.RegisterClassHandler(typeof(GridSplitter2), Thumb.DragCompletedEvent, new DragCompletedEventHandler(_OnDragCompleted)); EventManager.RegisterClassHandler(typeof(GridSplitter2), Thumb.DragDeltaEvent, new DragDeltaEventHandler(_OnDragDelta)); } /// public GridSplitter2() { ResizeBehavior = GridResizeBehavior.PreviousAndNext; SnapsToDevicePixels = true; Focusable = false; } static void _OnDragStarted(object sender, DragStartedEventArgs e) => (sender as GridSplitter2)._OnDragStarted(e); void _OnDragStarted(DragStartedEventArgs e) { if (!ShowsPreview) e.Handled = true; if (!_Init()) base.CancelDrag(); } static void _OnDragCompleted(object sender, DragCompletedEventArgs e) => (sender as GridSplitter2)._OnDragCompleted(e); void _OnDragCompleted(DragCompletedEventArgs e) { if (!ShowsPreview) e.Handled = true; //else somehow GridSplitter does not resize, just removes the adorner if (_a == null) return; //two events if called CancelDrag if (!e.Canceled) _MoveSplitter(); _a = null; } static void _OnDragDelta(object sender, DragDeltaEventArgs e) => (sender as GridSplitter2)._OnDragDelta(e); void _OnDragDelta(DragDeltaEventArgs e) { _delta = _isVertical ? e.HorizontalChange : e.VerticalChange; var di = DragIncrement; _delta = Math.Round(_delta / di) * di; if (ShowsPreview) return; e.Handled = true; if (_working) return; _working = true; //avoid too much CPU and delayed repainting of hwndhosts Dispatcher.InvokeAsync(() => _working = false, System.Windows.Threading.DispatcherPriority.ApplicationIdle); _MoveSplitter(); } bool _working; /// protected override void OnKeyDown(KeyEventArgs e) { if (e.Key == Key.Up || e.Key == Key.Down || e.Key == Key.Left || e.Key == Key.Right) { e.Handled = true; if (!_Init()) return; _delta = KeyboardIncrement * ((e.Key == Key.Up || e.Key == Key.Right) ? -1 : 1); if (_isVertical && FlowDirection == FlowDirection.RightToLeft) _delta = -_delta; _MoveSplitter(); _a = null; } else if (e.Key == Key.Escape && _a != null) { e.Handled = true; CancelDrag(); } } bool _Init(Key key = default) { _a = null; _isVertical = _IsVertical(); if (key != default && _isVertical != (key == Key.Left || key == Key.Right)) return false; //_resizeBehavior=_GetResizeBehavior(_isVertical); if (_GetResizeBehavior(_isVertical) != GridResizeBehavior.PreviousAndNext) throw new NotSupportedException("ResizeBehavior must be PreviousAndNext."); _delta = 0; _grid = Parent as Grid; _a = new List<_RowCol>(); _index = 0; var splitters = _grid.Children.OfType() .Where(o => o._IsVertical() == _isVertical) .Select(o => _IndexInGrid(o)).ToArray(); int index = _IndexInGrid(this); for (int i = 0, n = (_isVertical ? _grid.ColumnDefinitions.Count : _grid.RowDefinitions.Count); i < n; i++) { var v = new _RowCol(_isVertical ? (DefinitionBase)_grid.ColumnDefinitions[i] : _grid.RowDefinitions[i]); if (splitters.Contains(i)) { if (v.IsStar) throw new InvalidOperationException("Splitter row/column cannot be star-sized."); if (i == index) _index = _a.Count; } else { if (v.Unit == GridUnitType.Auto) continue; if (v.IsStar) v.SetSize(v.ActualSize); _a.Add(v); } } if (_index == 0 || _index == _a.Count) { //no resizable items before or after _a = null; return false; } return true; } Grid _grid; bool _isVertical; // GridResizeBehavior _resizeBehavior; List<_RowCol> _a; //resizable rows/columns, ie those without splitters and not auto-sized int _index; //index of first _a item after this splitter double _delta; void _MoveSplitter() { if (_a == null || _delta == 0) return; _Side before = default, after = default; //resize multiple star-sized items at that side? if (ResizeNearest || Keyboard.Modifiers == ModifierKeys.Control) { before.single = after.single = true; } else { int stars = 0; //flags: 1 stars before, 2 stars after for (int i = 0; i < _a.Count; i++) if (_a[i].IsStar) stars |= i < _index ? 1 : 2; before.single = _index == 1 || 0 == (stars & 1) || _a[_index - 1].ActualSize < 4 || (stars == 3 && !_a[_index - 1].IsStar); //without the last || subexpression would be impossible to resize fixed-sized items if there are star-sized items at both sides after.single = _index == _a.Count - 1 || 0 == (stars & 2) || _a[_index].ActualSize < 4 || (stars == 3 && !_a[_index].IsStar); } for (int i = 0; i < _a.Count; i++) { if (!_IsResizable(i)) continue; if (i < _index) before.Add(_a[i]); else after.Add(_a[i]); } double v1 = Math.Clamp(before.size + _delta, before.min, before.max), v2 = Math.Clamp(after.size - _delta, after.min, after.max); _delta = 0; if (v1 == before.min || v1 == before.max) v2 = before.size + after.size - v1; else if (v2 == after.min || v2 == after.max) v1 = before.size + after.size - v2; _ResizeSide(before, true, v1); _ResizeSide(after, false, v2); void _ResizeSide(_Side side, bool isBefore, double size) { if (side.single) { _a[_index - (isBefore ? 1 : 0)].SetSize(size); } else { for (int i = isBefore ? 0 : _index, to = isBefore ? _index : _a.Count; i < to; i++) { if (!_IsResizable(i)) continue; var v = _a[i]; var k = size * v.ActualSize; if (side.size > 0.1) k /= side.size; else k = 0.1; v.SetSize(k); } } } bool _IsResizable(int index) { if (index < _index) return before.single ? index == _index - 1 : _a[index].IsStar; return after.single ? index == _index : _a[index].IsStar; } } struct _Side { public double size, min, max; public bool single; public int stars; public void Add(_RowCol v) { size += v.ActualSize; min += v.Min; double x = v.Max; if (max != double.PositiveInfinity) { if (x == double.PositiveInfinity) max = x; else max += x; } if (!single && v.IsStar) stars++; } } /// /// Always resize only the nearest resizable row/column at each side. /// If false (default), may resize multiple star-sized rows/columns, unless with Ctrl key. /// public bool ResizeNearest { get; set; } #region util bool _IsVertical() { //see code of GridSplitter.GetEffectiveResizeDirection. The algorithm is documented. var dir = this.ResizeDirection; if (dir != GridResizeDirection.Auto) return dir == GridResizeDirection.Columns; if (this.HorizontalAlignment != HorizontalAlignment.Stretch) return true; if (this.VerticalAlignment != VerticalAlignment.Stretch) return false; return this.ActualWidth <= this.ActualHeight; } GridResizeBehavior _GetResizeBehavior(bool vertical) { //see code of GridSplitter.GetEffectiveResizeBehavior var r = ResizeBehavior; if (r == GridResizeBehavior.BasedOnAlignment) { if (vertical) r = HorizontalAlignment switch { HorizontalAlignment.Left => GridResizeBehavior.PreviousAndCurrent, HorizontalAlignment.Right => GridResizeBehavior.CurrentAndNext, _ => GridResizeBehavior.PreviousAndNext, }; else r = VerticalAlignment switch { VerticalAlignment.Top => GridResizeBehavior.PreviousAndCurrent, VerticalAlignment.Bottom => GridResizeBehavior.CurrentAndNext, _ => GridResizeBehavior.PreviousAndNext, }; } return r; } int _IndexInGrid(UIElement e) => _isVertical ? Grid.GetColumn(e) : Grid.GetRow(e); class _RowCol { RowDefinition _row; ColumnDefinition _col; public _RowCol(DefinitionBase def) { _row = def as RowDefinition; _col = def as ColumnDefinition; Min = _row?.MinHeight ?? _col.MinWidth; Max = _row?.MaxHeight ?? _col.MaxWidth; Unit = DefSizeGL.GridUnitType; } public double ActualSize => _row?.ActualHeight ?? _col.ActualWidth; public double DefSize { get => _row?.Height.Value ?? _col.Width.Value; // set { DefSizeGL = new GridLength(value, Unit); } } GridLength DefSizeGL { get => _row?.Height ?? _col.Width; // set { if(_row!=null) _row.Height=value; else _col.Width=value; } } public void SetSize(double size) { var z = new GridLength(size, Unit); if (_row != null) _row.Height = z; else _col.Width = z; } public GridUnitType Unit { get; private set; } public bool IsStar => Unit == GridUnitType.Star; public double Min { get; private set; } public double Max { get; private set; } } #endregion } /// /// Adorner that draws watermark/hint/cue text over the adorned control ( etc). /// public class WatermarkAdorner : Adorner { string _text; Control _c; TextBox _tCB; /// /// Initializes and adds this adorner to the found by . /// /// The control. /// Watermark text. /// /// In some kinds of windows it may not work unless a parent or ancestor of the control is an . /// public WatermarkAdorner(Control c, string text) : base(c) { _c = c; _text = text; IsHitTestVisible = false; if (AdornerLayer.GetAdornerLayer(c) is { } layer) layer.Add(this); else c.Loaded += (_, _) => { AdornerLayer.GetAdornerLayer(c)?.Add(this); }; //the Window or some ancestor element usually has an AdornerLayer in its template, which now probably still not applied } /// /// Gets or sets watermark text. /// public string Text { get => _text; set { if (value != _text) { _text = value; InvalidateVisual(); } } } /// /// Sets events to show/hide the adorner depending on control text. /// The control must be , or editable . /// public void SetAdornerVisibility() { if (_c is TextBox t) { _VisibilityT(t); } else if (_c is PasswordBox p) { _VisibilityP(p); } else { if (_c.IsLoaded) _OfChildTB(); else _c.Loaded += (_, _) => { _OfChildTB(); }; void _OfChildTB() { if (_c.FindVisualDescendant(o => o is TextBox) is TextBox t2) _VisibilityT(_tCB = t2); } } void _VisibilityT(TextBox t) { Visibility = t.Text.NE() ? Visibility.Visible : Visibility.Hidden; t.TextChanged += (_, _) => { Visibility = t.Text.NE() ? Visibility.Visible : Visibility.Hidden; }; } void _VisibilityP(PasswordBox t) { Visibility = t.Password.NE() ? Visibility.Visible : Visibility.Hidden; t.PasswordChanged += (_, _) => { Visibility = t.Password.NE() ? Visibility.Visible : Visibility.Hidden; }; } } /// protected override void OnRender(DrawingContext dc) { if (_text.NE()) return; var tf = new Typeface(_c.FontFamily, _c.FontStyle, _c.FontWeight, _c.FontStretch); var ft = new FormattedText(_text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, tf, _c.FontSize, Brushes.DarkGray, 96); Thickness bt = _c.BorderThickness, pad = _c.Padding; Point p = new(bt.Left + pad.Left + 2, bt.Top + pad.Top); var z = _c.RenderSize; double w = z.Width - bt.Right - pad.Right - 2, h = z.Height - bt.Bottom - pad.Bottom; if (_tCB != null) w = _tCB.RenderSize.Width; if (w < 4 || h < 4) return; dc.PushClip(new RectangleGeometry(new(0, 0, w, h))); dc.DrawText(ft, p); dc.Pop(); } } } ================================================ FILE: Au/GUI/wpfBuilder.cs ================================================ using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Documents; using System.Windows.Media; using System.Windows.Data; using System.Windows.Input; using System.Xml.Linq; //for using C = System.Windows.Controls; using W = System.Windows; using D = System.Windows.Documents; //never mind: on Win7 text of all WPF checkboxes too low by 1 pixel. Not only of wpfBuilder. namespace Au; #pragma warning disable WPF0001 //enable ThemeMode /// /// With this class you can create windows with controls, for example for data input. /// /// /// This class uses WPF (Windows Presentation Foundation). Creates window at run time. No designer. No WPF and XAML knowledge required, unless you want something advanced. /// /// To start, use snippet wpfDialogSnippet or menu File > New > Dialogs. Also look in Cookbook. /// /// Most functions return this, to enable method chaining, aka fluent interface, like with . See example. /// /// A wpfBuilder object can be used to create whole window or some window part, for example a tab page. /// /// The size/position unit in WPF is about 1/96 inch, regardless of screen DPI. For example, if DPI is 96 (100%), 1 unit = 1 physical pixel; if 150% - 1.5 pixel; if 200% - 2 pixels. WPF windows are DPI-scaled automatically when need. Your program's manifest should contain dpiAware=true/PM and dpiAwareness=PerMonitorV2; it is default for scripts/programs created with the script editor of this library. /// /// Note: WPF starts slowly and uses much memory. It is normal if to show the first window in process takes 500-1000 ms and the process uses 30 MB of memory, whereas WinForms takes 250 ms / 10 MB and native takes 50 ms / 2 MB. However WinForms becomes slower than WPF if there are more than 100 controls in window. This library uses WPF because it is the most powerful and works well with high DPI screens. /// /// WPF has many control types, for example , , , , . Most are in namespaces System.Windows.Controls and System.Windows.Controls.Primitives. Also on the internet you can find many libraries containing WPF controls and themes. For example, search for github awesome dotnet C#. Many libraries are open-source, and most can be found in GitHub (source, info and sometimes compiled files). Compiled files usually can be found in as packages. Use menu Tools > NuGet. /// /// By default don't need XAML. When need, you can load XAML strings and files with . /// /// /// Dialog window with several controls for data input. /// /// public class wpfBuilder { //readonly FrameworkElement _container; //now used only in ctor readonly Window _window; //= _container or null _PanelBase _p; //current grid/stack/dock/canvas panel, either root or nested class _PanelBase { protected readonly wpfBuilder _b; public readonly _PanelBase parent; public readonly Panel panel; FrameworkElement _lastAdded, _lastAdded2; public bool ended; public _PanelBase(wpfBuilder b, Panel p) { _b = b; parent = b._p; _lastAdded = panel = p; } public virtual void BeforeAdd(bool childOfLast) { if (ended) throw new InvalidOperationException("Cannot add after End()"); if (childOfLast && _lastAdded == panel) throw new ArgumentException("Cannot add as child. The last element is panel."); } public virtual void Add(FrameworkElement c) { SetLastAdded(c); panel.Children.Add(c); } public virtual void End() { ended = true; } public FrameworkElement LastAdded => _lastAdded; public FrameworkElement LastAdded2 => _lastAdded2; public void SetLastAdded(FrameworkElement e) { if (_lastAdded != panel) _lastAdded2 = _lastAdded; _lastAdded = e; } public FrameworkElement LastDirect { get { if (_lastAdded == panel) { Debug_.Print("lastAdded == panel"); return null; } for (var c = _lastAdded; ;) { var pa = c.Parent as FrameworkElement; if (pa == panel) return c; c = pa; } } } } class _Canvas : _PanelBase { public _Canvas(wpfBuilder b, Canvas c = null) : base(b, c ?? new Canvas()) { panel.HorizontalAlignment = HorizontalAlignment.Left; panel.VerticalAlignment = VerticalAlignment.Top; } } class _DockPanel : _PanelBase { public _DockPanel(wpfBuilder b) : base(b, new DockPanel()) { } } class _StackPanel : _PanelBase { public _StackPanel(wpfBuilder b, bool vertical) : base(b, new StackPanel { Orientation = vertical ? Orientation.Vertical : Orientation.Horizontal }) { } } class _Grid : _PanelBase { readonly Grid _grid; //same as panel, just to avoid casting everywhere int _row = -1, _col; bool _isSpan; double? _andWidth; public _Grid(wpfBuilder b, Grid g = null) : base(b, g ?? new Grid()) { _grid = panel as Grid; if (gridLines) _grid.ShowGridLines = true; } public void Row(WBGridLength height) { if (_andWidth != null) throw new InvalidOperationException("And().Row()"); if (_row >= 0) { _SetLastSpan(); _col = 0; } else if (_grid.ColumnDefinitions.Count == 0) { _grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(0, GridUnitType.Auto) }); _grid.ColumnDefinitions.Add(new ColumnDefinition()); } _row++; _grid.RowDefinitions.Add(height.Row); } public override void BeforeAdd(bool childOfLast) { base.BeforeAdd(childOfLast); if (childOfLast) return; if (_row < 0 || _col >= _grid.ColumnDefinitions.Count) Row(0); _isSpan = false; } public override void Add(FrameworkElement c) { if (_andWidth != null) { var width = _andWidth.Value; _andWidth = null; if (width < 0) { var m = c.Margin; m.Left += -width + 3; c.Margin = m; } else if (width > 0) { c.Width = width; c.HorizontalAlignment = HorizontalAlignment.Right; } var last = LastDirect; Grid.SetColumn(c, Grid.GetColumn(last)); Grid.SetColumnSpan(c, Grid.GetColumnSpan(last)); _isSpan = true; } else { Grid.SetColumn(c, _col); } _col++; Grid.SetRow(c, _row); base.Add(c); } public void And(double width) { if (_col == 0 || _andWidth != null || LastAdded == panel) throw new InvalidOperationException("And()"); var c = LastDirect; if (width < 0) { c.Width = -width; c.HorizontalAlignment = HorizontalAlignment.Left; } else if (width > 0) { var m = c.Margin; m.Right += width + 3; c.Margin = m; } _andWidth = width; _col--; } public void Span(int span) { if (_col == 0) throw new InvalidOperationException("Span() at row start"); int cc = _grid.ColumnDefinitions.Count; _col--; if (span != 0) { //if 0, will add 2 controls in 1 cell if (span < 0 || _col + span > cc) span = cc - _col; Grid.SetColumnSpan(LastDirect, span); _col += span; } _isSpan = true; } //If not all row cells filled, let the last control span all remaining cells, unless its span specified explicitly. void _SetLastSpan() { if (!_isSpan && _row >= 0 && _col > 0) { int n = _grid.ColumnDefinitions.Count - _col; if (n > 0) Grid.SetColumnSpan(LastDirect, n + 1); } _isSpan = false; } public void Skip(int span = 1) { BeforeAdd(false); _col += span; _isSpan = true; } public override void End() { base.End(); _SetLastSpan(); } public (int column, int row) NextCell => (_col, _row); } #region current panel /// /// Ends adding controls etc to the window or nested panel ( etc). /// /// /// Always call this method to end a nested panel. For root panel it is optional if using . /// public wpfBuilder End() { if (!_p.ended) { _p.End(); if (_p.parent != null) { _p = _p.parent; } else { #if NET9_0_OR_GREATER if (_unGray && _window is { } w && w.ThemeMode != ThemeMode.None && w.Background == SystemColors.ControlBrush) w.Background = null; #endif } } return this; } /// /// Sets column count and widths of current grid. /// /// /// Column widths. /// An argument can be: ///
int or double - . Value 0 means auto-size. Negative value is star-width, ie fraction of total width of star-sized columns. Examples: 50, -0.5. ///
- and/or . Sets width value = -1 (star-sized). Examples: 50..150, 50.. or ..150. ///
• tuple (double value, Range minMax) - width and min/max widths. Example: (-2, 50..). ///
. /// /// Columns() in non-grid panel or after an Add function. /// /// If this function not called, the table has 2 columns like .Columns(0, -1). /// /// If there are star-sized columns, should be set width of the grid or of its container. Call or or . But if the grid is in a cell of another grid, usually it's better to set column width of that grid to a non-zero value, ie let it be not auto-sized. /// public wpfBuilder Columns(params WBGridLength[] widths) { var g = Last as Grid ?? throw new InvalidOperationException("Columns() in wrong place"); g.ColumnDefinitions.Clear(); foreach (var v in widths) g.ColumnDefinitions.Add(v.Column); return this; } /// /// Starts new row in current grid. /// /// /// Row height. Can be: ///
int or double - . Value 0 means auto-size. Negative value is star-width, ie fraction of total height of star-sized rows. Examples: 50, -0.5. ///
- and/or . Sets height value = -1 (star-sized). Examples: 50..150, 50.. or ..150. ///
• tuple (double value, Range minMax) - height and min/max heights. Example: (-2, 50..200). ///
. /// /// In non-grid panel. /// /// Calling this function is optional, except when not all cells of previous row are explicitly filled. /// /// If there are star-sized rows, grid height should be defined. Call or . But if the grid is in a cell of another grid, usually it's better to set row height of that grid to a non-zero value, ie let it be not auto-sized. /// public wpfBuilder Row(WBGridLength height) { if (_p.ended) throw new InvalidOperationException("Row() after End()"); var g = _p as _Grid ?? throw new InvalidOperationException("Row() in non-grid panel"); g.Row(height); return this; } /// /// Starts new auto-sized row in current grid. The same as Row(0). See . /// /// In non-grid panel. public wpfBuilder R => Row(0); #endregion #region ctors, window // static readonly DependencyProperty _wpfBuilderProperty = DependencyProperty.RegisterAttached("_wpfBuilder", typeof(wpfBuilder), typeof(Panel)); static ConditionalWeakTable s_cwt = new(); //which is better? Both fast. /// /// This constructor creates object with panel of specified type (default is ). /// /// Window title bar text. /// Panel type. Default is . Later you also can add nested panels of various types with StartX functions. public wpfBuilder(string windowTitle, WBPanelType panelType = WBPanelType.Grid) { /*_container=*/ _window = new Window() { Title = windowTitle }; _AddRootPanel(_window, false, panelType, true); } /// /// This constructor creates panel of specified type (default is ) and optionally adds to a container. /// /// /// Window or some other element that will contain the panel. Should be empty, unless the type supports multiple direct child elements. Can be null. /// If the type (or base type) is (, , , etc), or (eg Border), this function adds the panel to it. If container is null or an element of some other type, need to explicitly add the panel to it, like container.Child = b.Panel; or container.Children.Add(b.Panel); or b.Tooltip(btt.Panel); or hwndSource.RootVisual = btt.Panel; (the code depends on container type). /// /// Panel type. Default is . Later you also can add nested panels of various types with StartX functions. /// /// Set some container's properties like other overload does. Default true. Currently sets these properties, and only if container is of type Window: ///
, except when container is Canvas or has properties Width and/or Height set. ///
SnapsToDevicePixels = true. ///
WindowStartupLocation = Center. ///
Topmost and Background depending on static properties and . /// public wpfBuilder(FrameworkElement container = null, WBPanelType panelType = WBPanelType.Grid, bool setProperties = true) { //_container=container; // ?? throw new ArgumentNullException("container"); //can be null _window = container as Window; _AddRootPanel(container, true, panelType, setProperties); } void _AddRootPanel(FrameworkElement container, bool externalContainer, WBPanelType panelType, bool setProperties) { _p = panelType switch { WBPanelType.Grid => new _Grid(this), WBPanelType.Canvas => new _Canvas(this), WBPanelType.Dock => new _DockPanel(this), WBPanelType.HorizontalStack => new _StackPanel(this, false), WBPanelType.VerticalStack => new _StackPanel(this, true), _ => throw new InvalidEnumArgumentException() }; if (_window != null) _p.panel.Margin = new(3); switch (container) { case ContentControl c: c.Content = _p.panel; break; case Popup c: c.Child = _p.panel; break; case Decorator c: c.Child = _p.panel; break; //rejected. Rare. Let users add explicitly, like container.Child = b.Panel. // case Panel c: c.Children.Add(_p.panel); break; // case ItemsControl c: c.Items.Add(_p.panel); break; // case TextBlock c: c.Inlines.Add(_p.panel); break; // default: throw new NotSupportedException("Unsupported container type"); } if (setProperties) { if (_window != null) { if (panelType != WBPanelType.Canvas) { if (externalContainer) { _window.SizeToContent = (double.IsNaN(_window.Width) ? SizeToContent.Width : 0) | (double.IsNaN(_window.Height) ? SizeToContent.Height : 0); } else { _window.SizeToContent = SizeToContent.WidthAndHeight; } } _window.SnapsToDevicePixels = true; //workaround for black line at bottom, for example when there is single CheckBox in Grid. //_window.UseLayoutRounding=true; //not here. Makes many controls bigger by 1 pixel when resizing window with grid, etc. Maybe OK if in _Add (for each non-panel element). if (_window.WindowStartupLocation == default) _window.WindowStartupLocation = WindowStartupLocation.CenterScreen; if (winTopmost) _window.Topmost = true; if (!winWhite) { _window.Background = SystemColors.ControlBrush; _unGray = true; //rejected: remove this background brush when _window.ThemeMode property changed. Slow. Instead remove in End(), if _unGray still true. //#if NET9_0_OR_GREATER // DependencyPropertyDescriptor.FromProperty(Window.OverridesDefaultStyleProperty, typeof(Window)).AddValueChanged(_window, static (o, _) => { // if (o is Window w && w.ThemeMode != ThemeMode.None && w.Background == SystemColors.ControlBrush) w.Background = null; // }); //#endif } } } s_cwt.Add(_p.panel, this); if (script.role == SRole.MiniProgram && _window != null) Loaded += () => { }; //set custom icon if need } #pragma warning disable CS0414 //when compiling for nuget .NET 8 bool _unGray; #pragma warning restore CS0414 /// /// Shows the window and waits until closed. /// /// Owner window or element. Sets . /// /// - Container is not of type Window. /// - Missing for a panel added with a StartX function. /// /// /// Calls , sets and calls . /// You can instead call these functions directly. Or call to show as non-modal window, ie don't wait. Or add to some container window or other element, etc. /// public bool ShowDialog(DependencyObject owner = null) { _ThrowIfNotWindow(); if (_IsNested) throw new InvalidOperationException("Missing End() for a StartX() panel"); End(); //if (script.isWpfPreview) _window.Preview(); //no _window.Owner = owner == null ? null : owner as Window ?? Window.GetWindow(owner); //TODO3: try to support AnyWnd. Why WPF here supports only Window and not Popup or HwndSource? return true == _window.ShowDialog(); } /// /// Sets window width and/or height or/and min/max width/height. /// /// Width or/and min/max width. /// Height or/and min/max height. /// /// - Container is not of type Window. /// - Cannot be after the last . /// - Cannot be after or . /// /// /// Use WPF logical device-independent units, not physical pixels. /// /// /// public wpfBuilder WinSize(WBLength? width = null, WBLength? height = null) { _ThrowIfNotWindow(); _ThrowIfWasWinRect(); if (_IsWindowEnded) throw new InvalidOperationException("WinSize() cannot be after last End()"); //although currently could be anywhere var u = _window.SizeToContent; if (width != null) { var v = width.Value; v.ApplyTo(_window, false); u &= ~SizeToContent.Width; } if (height != null) { var v = height.Value; v.ApplyTo(_window, true); u &= ~SizeToContent.Height; } _window.SizeToContent = u; return this; } void _ThrowIfWasWinRectXY([CallerMemberName] string m_ = null) { if (_wasWinXY != 0) throw new InvalidOperationException(m_ + " cannot be after WinXY, WinRect or WinSaved."); } void _ThrowIfWasWinRect([CallerMemberName] string m_ = null) { if (_wasWinXY == 2) throw new InvalidOperationException(m_ + " cannot be after WinRect or WinSaved."); } byte _wasWinXY; //1 xy, 2 rect /// /// Sets window location. /// /// X coordinate in screen. Physical pixels. /// Y coordinate in screen. Physical pixels. /// /// - Container is not of type Window. /// - Cannot be after , or . /// /// /// With this function use physical pixels, not WPF logical device-independent units. /// Call this function before showing the window. Don't change location/size-related window properties after that. /// Calls . /// /// public wpfBuilder WinXY(int x, int y) { _ThrowIfNotWindow(); _ThrowIfWasWinRectXY(); _wasWinXY = 1; _window.SetXY(x, y); return this; } /// /// Sets window rectangle (location and size). /// /// Rectangle in screen. Physical pixels. /// /// - Container is not of type Window. /// - Cannot be after , or . /// /// /// With this function use physical pixels, not WPF logical device-independent units. /// Call this function before showing the window. Don't change location/size-related window properties after that. /// Calls . /// /// public wpfBuilder WinRect(RECT r) { _ThrowIfNotWindow(); _ThrowIfWasWinRectXY(); _wasWinXY = 2; _window.SetRect(r); return this; } /// /// Saves window xy/size/state when closing and restores when opening. /// /// String that the save action received previously. Can be null or "", usually first time (still not saved). /// Called when closing the window. Receives string containing window xy/size/state. Can save it in registry, file, anywhere. /// /// - Container is not of type Window. /// - Cannot be after , or . /// - Window is loaded. /// /// /// Calls . /// Call this function before showing the window. Don't change location/size-related window properties after that. /// If you use , call it before. It is used if size is still not saved. The same if you set window position or state. /// /// /// Microsoft.Win32.Registry.SetValue(rk, rv, o)); /// b.End(); /// ]]> /// public wpfBuilder WinSaved(string saved, Action save) { _ThrowIfNotWindow(); _ThrowIfWasWinRectXY(); _wasWinXY = 2; WndSavedRect.Restore(_window, saved, save); return this; } /// /// Changes various window properties. /// /// Sets . /// Sets . /// Sets . /// Sets . /// Sets . /// Sets . /// Sets . /// Sets . Example: .WinProperties(icon: BitmapFrame.Create(new Uri(@"d:\icons\file.ico"))). /// Set background color: true (white), false (gray). See also , . /// /// - Container is not of type Window. /// - startLocation or state used after , or . /// /// /// The function uses only non-null parameters. /// Or you can change properties directly, for example b.Window.Topmost = true;. /// public wpfBuilder WinProperties(WindowStartupLocation? startLocation = null, ResizeMode? resizeMode = null, bool? showActivated = null, bool? showInTaskbar = null, bool? topmost = null, WindowState? state = null, WindowStyle? style = null, ImageSource icon = null, bool? whiteBackground = null) { _ThrowIfNotWindow(); if (startLocation.HasValue) { _ThrowIfWasWinRectXY("WinProperties(startLocation)"); _window.WindowStartupLocation = startLocation.Value; } if (resizeMode.HasValue) _window.ResizeMode = resizeMode.Value; if (showActivated.HasValue) _window.ShowActivated = showActivated.Value; if (showInTaskbar.HasValue) _window.ShowInTaskbar = showInTaskbar.Value; if (topmost.HasValue) _window.Topmost = topmost.Value; if (state.HasValue) { _ThrowIfWasWinRectXY("WinProperties(state)"); _window.WindowState = state.Value; } if (style.HasValue) _window.WindowStyle = style.Value; if (whiteBackground.HasValue) { _window.Background = whiteBackground.Value ? SystemColors.WindowBrush : SystemColors.ControlBrush; _unGray = false; } if (icon != null) _window.Icon = icon; return this; } #endregion #region properties, events /// /// Gets the top-level window. /// /// null if container is not of type Window. public Window Window => _window; /// /// Gets current or or etc. /// public Panel Panel => _p.panel; /// /// Gets the last child or descendant element added in current panel. Before that returns current panel. /// /// /// The "set properties of last element" functions set properties of this element. /// public FrameworkElement Last => _p.LastAdded; /// /// Gets the child or descendant element added in current panel before adding . Can be null. /// /// /// For example, after calling an Add overload that adds 2 elements (the first is ), this property returns the . /// public FrameworkElement Last2 => _p.LastAdded2; // not useful // /// // /// Gets the last direct child element added in current panel. Before that returns current panel or its parent . // /// // public FrameworkElement LastDirect => _p.LastDirect; /// /// When root panel loaded and visible. Once. /// /// /// If the panel is in a , this event is fired when the tab page is selected/loaded first time. /// When this event is fired, handles of visible -based controls are already created. /// public event Action Loaded { add { if (!_loadedEvent2) { _loadedEvent2 = true; Panel.Loaded += _Panel_Loaded; } _loadedEvent += value; } remove { _loadedEvent -= value; } } Action _loadedEvent; bool _loadedEvent2; private void _Panel_Loaded(object sender, RoutedEventArgs e) { var p = sender as Panel; if (!p.IsVisible) return; p.Loaded -= _Panel_Loaded; //if role miniProgram, use assembly icon instead of apphost icon if (script.role == SRole.MiniProgram && _window != null && _window.Icon == null) { var hm = Api.GetModuleHandle(Assembly.GetEntryAssembly().Location); if (default != Api.FindResource(hm, Api.IDI_APPLICATION, Api.RT_GROUP_ICON)) { var w = _window.Hwnd(); icon.FromModuleHandle_(hm, Dpi.Scale(16, w))?.SetWindowIcon(w, false); icon.FromModuleHandle_(hm, Dpi.Scale(32, w))?.SetWindowIcon(w, true); } } _loadedEvent?.Invoke(); } /// /// When clicked OK or Apply button. /// /// /// is true if it is OK button. /// The parameter's property Cancel can be used to prevent closing the window. /// public event Action OkApply; #endregion #region static /// /// of grid panels created afterwards. /// To be used at design time only. /// public static bool gridLines { get; set; } /// /// of windows created afterwards. /// Usually used at design time only, to make always on top of editor window. /// public static bool winTopmost { get; set; } /// /// If true, wpfBuilder constructors used afterwards will not change the window background color. /// /// /// If true, wpfBuilder constructors don't change the background color; then the color depends on theme. /// If false constructors set standard color of dialog windows, usually light gray. /// Default value depends on application's theme and usually is true if using a custom theme. /// public static bool winWhite { get => s_winWhite ?? (_GetThemed() != _Themed.None); set { s_winWhite = value; } } static bool? s_winWhite; // /// // /// Default modifyPadding option value. See . // /// // public static bool modifyPadding { get; set; } #endregion #region add /// /// Changes some options for elements added afterwards. /// /// Let Add adjust the Padding property of some controls to align content better when using default theme. Default value of this option depends on application's theme. /// Right-align controls in grid cells. /// Default margin of elements. If not set, default margin is 3 in all sides. Default margin of nested panels is 0; this option is not used. /// Show tooltips when the tooltip owner element receives the keyboard focus when using keys to focus controls or open the window. If true, it can be set separately for each tooltip or owner element with or . /// Let and an Add overload that adds 2 elements (the first is ) bind the Visibility property of the label to that of the last added element, to automatically hide/show the label together with the element. Also binds IsEnabled if it is or . public wpfBuilder Options(bool? modifyPadding = null, bool? rightAlignLabels = null, Thickness? margin = null, bool? showToolTipOnKeyboardFocus = null, bool? bindLabelVisibility = null) { if (modifyPadding != null) _opt_modifyPadding = modifyPadding.Value; if (rightAlignLabels != null) _opt_rightAlignLabels = rightAlignLabels.Value; if (margin != null) _opt_margin = margin.Value; if (showToolTipOnKeyboardFocus != null) _opt_showToolTipOnKeyboardFocus = showToolTipOnKeyboardFocus.Value; if (bindLabelVisibility != null) _opt_bindLabelVisibility = bindLabelVisibility.Value; return this; } bool? _opt_modifyPadding; bool _opt_rightAlignLabels; Thickness _opt_margin = new(3); //string _opt_radioGroup; //rejected. Radio buttons have problems with high DPI and should not be used. Or can put groups in panels. // double _opt_checkMargin=3; //rejected bool _opt_showToolTipOnKeyboardFocus; bool _opt_bindLabelVisibility; bool? _ModifyPadding => _opt_modifyPadding == false ? false : _IsThemed switch { _Themed.Wpf => null, _Themed.Other => _opt_modifyPadding == true, _ => true }; void _Add(FrameworkElement e, object text, bool raw = false) { bool childOfLast = _child; _child = false; if (!raw) { if (e is Control c) { //rejected: modify padding etc through XAML. Not better than this. //rejected: use _opt_modifyPadding only if font Segoe UI. Tested with several fonts. switch (c) { case Label: switch (_ModifyPadding) { case true: c.Padding = new(1, 2, 1, 1); break; //default 5 case null: c.Padding = new(1); c.VerticalAlignment = VerticalAlignment.Center; break; } if (_opt_rightAlignLabels) c.HorizontalAlignment = HorizontalAlignment.Right; break; case TextBox: case PasswordBox: if (_ModifyPadding == true) c.Padding = new(2, 1, 1, 2); //default padding 0, height 18 break; case Button: if (text is string && _ModifyPadding == true) c.Padding = new(5, 1, 5, 2); //default 1 break; case ToggleButton: c.HorizontalAlignment = HorizontalAlignment.Left; //default stretch c.VerticalAlignment = VerticalAlignment.Center; //default top. Note: VerticalContentAlignment bad on Win7. //partial workaround for squint CheckBox/RadioButton when High DPI. // Without it, check mark size/alignment is different depending on control's xy. // With it at least all controls are equal, either bad (eg DPI 125%) or good (DPI 150%). // When bad, normal CheckBox check mark now still looks good. Only third state and RadioButtons look bad, but it is better than when controls look differently. // But now at 150% DPI draws thick border. //c.UseLayoutRounding=true; //c.SnapsToDevicePixels=true; //does not help break; case ComboBox cb: //Change padding because default Windows font Segoe UI is badly centered vertically. Too big space above text, and too big control height. //Tested: changed padding isn't the reason of different control heights or/and arrows when high DPI. if (cb.IsEditable) { if (_ModifyPadding == true) c.Padding = new(2, 1, 2, 2); //default (2) } else { if (_ModifyPadding == true) c.Padding = new(5, 2, 4, 3); //default (6,3,5,3) } break; } } else if (e is Image) { e.UseLayoutRounding = true; //workaround for blurred images } //workaround for: // 1. Blurred images in some cases. // 2. High DPI: Different height of controls of same class, eg TextBox, ComboBox. // 3. High DPI: Different height/shape/alignment of control parts, eg CheckBox/RadioButton check mark and ComboBox arrow. // Bad: on DPI 150% makes control borders 2-pixel. // Rejected. Thick border is very noticeable, especially TabControl. Different control sizes, v check mark and v arrow aren't so noticeable. Radio buttons and null checkboxes rarely used. Most my tested WPF programs don't use this. // e.UseLayoutRounding=true; // e.SnapsToDevicePixels=true; //does not help if (text != null) { switch (e) { case HeaderedContentControl u: u.Header = text; break; //GroupBox, Expander case HeaderedItemsControl u: u.Header = text; break; case ContentControl u: u.Content = text; break; //Label, buttons, etc case TextBox u: u.Text = text.ToString(); break; case PasswordBox u: u.Password = text.ToString(); break; case ComboBox u: u.Text = text.ToString(); break; case TextBlock u: u.Text = text.ToString(); break; case RichTextBox u: u.AppendText(text.ToString()); break; //default: throw new NotSupportedException($"Add() cannot set text/content of {e.GetType().Name}."); default: if (!_PropGetSet.TryCreate(e, "Text", out var pgs)) throw new NotSupportedException($"Add() cannot set text/content of {e.GetType().Name}."); pgs.Set(text.ToString()); break; } } } if (!(childOfLast || e is GridSplitter)) e.Margin = _opt_margin; _AddToParent(e, childOfLast); if (_alsoAll != null) { _alsoAllArgs ??= new WBAlsoAllArgs(); if (_p is _Grid g) { var v = g.NextCell; _alsoAllArgs.Column = v.column - 1; _alsoAllArgs.Row = v.row; } else { _alsoAllArgs.Column = _alsoAllArgs.Row = -1; } _alsoAll(this, _alsoAllArgs); } } void _AddToParent(FrameworkElement e, bool childOfLast) { if (childOfLast) { //info: BeforeAdd throws exception if Last is panel switch (Last) { case ContentControl d: d.Content = e; break; case Decorator d: d.Child = e; break; //case Panel d: d.Children.Add(e); break; //no, cannot add multiple items because Last becomes the added child default: throw new NotSupportedException($"Cannot add child to {Last.GetType().Name}."); } _p.SetLastAdded(e); } else { _p.Add(e); } } /// /// Sets option for the next Add call to add the element as a child or content of the last added element (), which must be a (for example or ) or (for example ). Also will not change the Margin property. /// /// The last added element is not of a supported type. /// /// Also applies to functions that call Add, for example AddButton. Does not affect the obsolete hidden Add overloads with parameter flags. /// /// /// ().Border(Brushes.Green).Child().Add(out TextBlock t1, "Text").Margin("L3R3"); /// ]]> /// public wpfBuilder Child() { if (!(Last is ContentControl or Decorator)) throw new NotSupportedException($"Cannot add child to {Last.GetType().Name}."); _child = true; return this; } bool _child; /// /// Adds an existing element. /// /// /// Don't change element properties, except margin. If false (default), works like other Add overloads: for some element types sets padding, alignment, properties specified in , etc. /// /// /// /// public wpfBuilder Add(FrameworkElement element, bool raw = false) { _p.BeforeAdd(_child); _Add(element, null, raw); return this; } /// /// Creates and adds element of type T (control etc of any type). /// /// /// Receives element's variable. The function creates element of variable's type. You can use the variable to set element's properties before showing window or/and to get value after. /// Examples: .Add(out CheckBox c1, "Text"), .Add(out _textBox1). If don't need a variable: .Add(out Label _, "Text") or .Add<Label>("Text"). /// /// /// Text, header or other content. Supported element types (or base types): ///
- sets Text property. ///
- sets Text property (see also ). ///
- sets Password property. ///
- sets Text property (see also and ). ///
, - sets Header property (see also and ). ///
- sets Content property (can be string, other element, etc) (see also and ). ///
- calls AppendText (see also ). ///
• Other element types that have Text property. /// /// The function does not support non-null text for this element type. /// /// /// /// public wpfBuilder Add(out T variable, object text = null) where T : FrameworkElement, new() { _p.BeforeAdd(_child); variable = new T(); _Add(variable, text); return this; } /// /// Creates and adds element of type T (any type). This overload can be used when don't need element's variable. /// /// Text, header or other content. More info - see other overload. /// The function does not support non-null text for this element type. /// /// /// /// public wpfBuilder Add(object text = null) where T : FrameworkElement, new() => Add(out T _, text); /// /// Adds 2 elements: and element of type T (control etc of any type). /// /// Label text. Usually string or . Example: new TextBlock() { TextWrapping = TextWrapping.Wrap, Text = "long text" }. /// Variable of second element. More info - see other overload. /// Text, header or other content of second element. More info - see other overload. /// If not null, after adding first element calls with this argument. /// If the function does not support non-null text for this element type. /// /// Finally calls ; it sets , calls and applies the bindLabelVisibility option (see ). /// public wpfBuilder Add(object label, out T variable, object text = null, WBGridLength? row2 = null) where T : FrameworkElement, new() { if (_child) throw new InvalidOperationException(); Add(out Label var1, label); if (row2 != null) Row(row2.Value); Add(out variable, text); return this.LabeledBy(var1); } /// /// Adds 2 elements: and an existing element (control etc of any type). /// /// public wpfBuilder Add(object label, FrameworkElement element, WBGridLength? row2 = null) { if (_child) throw new InvalidOperationException(); Add(out Label var1, label); if (row2 != null) Row(row2.Value); Add(element); return this.LabeledBy(var1); } #if !DEBUG && NET9_0_OR_GREATER #pragma warning disable CS1591 //Missing XML comment for publicly visible type or member //[Obsolete("Instead of flags use: b.Child().Add(...)")] [EditorBrowsable(EditorBrowsableState.Never)] public wpfBuilder Add(FrameworkElement element, WBAdd flags) { _p.BeforeAdd(_child = flags.Has(WBAdd.ChildOfLast)); _Add(element, null, flags.Has(WBAdd.DontSetProperties)); return this; } //[Obsolete("Instead of flags use: b.Child().Add(...)")] [EditorBrowsable(EditorBrowsableState.Never)] [OverloadResolutionPriority(-1)] public wpfBuilder Add(out T variable, object text = null, WBAdd flags = 0) where T : FrameworkElement, new() { _p.BeforeAdd(_child = flags.Has(WBAdd.ChildOfLast)); variable = new T(); _Add(variable, text, flags.Has(WBAdd.DontSetProperties)); return this; } //[Obsolete("Instead of flags use: b.Child().Add(...)")] [EditorBrowsable(EditorBrowsableState.Never)] //[OverloadResolutionPriority(-1)] //no, then woul choose Add(out T variable, object text = null) public wpfBuilder Add(out T variable, WBAdd flags) where T : FrameworkElement, new() => Add(out variable, null, flags); //[Obsolete("Instead of flags use: b.Child().Add(...)")] [EditorBrowsable(EditorBrowsableState.Never)] [OverloadResolutionPriority(-1)] public wpfBuilder Add(object text = null, WBAdd flags = 0) where T : FrameworkElement, new() => Add(out T _, text, flags); [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete] //Too many overloads, confusing. Instead can use Last2 or LabeledBy. public wpfBuilder Add(out T1 var1, object text1, out T2 var2, object text2 = null, WBGridLength? row2 = null) where T1 : FrameworkElement, new() where T2 : FrameworkElement, new() { Add(out var1, text1); if (row2 != null) Row(row2.Value); Add(out var2, text2); //note: no flags return this.LabeledBy(var1); } #pragma warning restore CS1591 #endif /// /// Adds button with event handler. /// /// Receives button's variable. /// Text/content (). /// Action to call when the button clicked. Its parameter's property Cancel can be used to prevent closing the window when clicked this OK button. Not called if validation fails. /// /// /// If flags contains OK or Apply or Validate and this window contains elements for which was called , on click performs validation; if fails, does not call the click action and does not close the window. /// public wpfBuilder AddButton(out Button variable, object text, Action click, WBBFlags flags = 0/*, Action clickSplit = null*/) { Add(out variable, text); var c = variable; if (flags.Has(WBBFlags.OK)) c.IsDefault = true; if (flags.Has(WBBFlags.Cancel)) c.IsCancel = true; if (flags.HasAny(WBBFlags.OK | WBBFlags.Cancel | WBBFlags.Apply)) { c.MinWidth = 70; c.MinHeight = 21; } if (click != null || flags.HasAny(WBBFlags.OK | WBBFlags.Cancel | WBBFlags.Apply | WBBFlags.Validate)) { c.Click += (_, _) => { var w = _FindWindow(c); if (flags.HasAny(WBBFlags.OK | WBBFlags.Apply | WBBFlags.Validate) && !_Validate(w, c)) return; bool needEvent = flags.HasAny(WBBFlags.OK | WBBFlags.Apply) && OkApply != null; var e = (needEvent || click != null) ? new WBButtonClickArgs { Button = c, Window = w } : null; if (needEvent) { OkApply(e); if (e.Cancel) return; } if (click != null) { click(e); if (e.Cancel) return; } if (flags.Has(WBBFlags.OK)) { bool modal = w.IsModal_() != false; if (modal) { try { w.DialogResult = true; } catch (InvalidOperationException) { modal = false; } //failed to detect modal? } if (!modal) w.Close(); } else if (flags.Has(WBBFlags.Cancel)) { w.Close(); //info: IsCancel ignored if nonmodal } }; } //if(clickSplit!=null) c.ClickSplit+=clickSplit; //FUTURE: split-button. return this; } // /// // /// If not null, creates split-button. Action to call when the arrow part clicked. Example: // ///
b => { int mi = popupMenu.showSimple("1 One|2 Two", b, (0, b.Height)); } // /// /// public wpfBuilder AddButton(object text, Action click, WBBFlags flags = 0/*, Action clickSplit = null*/) { return AddButton(out _, text, click, flags); } /// /// Adds button that closes the window and sets . /// /// Text/content (). /// value when clicked this button. /// /// When clicked, sets = result, closes the window, and returns true. /// public wpfBuilder AddButton(object text, int result/*, Action clickSplit = null*/) { Add(out Button c, text); c.Click += (_, _) => { _resultButton = result; _FindWindow(c).DialogResult = true; }; //if(clickSplit!=null) c.ClickSplit+=clickSplit; return this; } // /// // /// If not null, creates split-button. Action to call when the arrow part clicked. Example: // ///
b => { int mi = popupMenu.showSimple("1 One|2 Two", b, (0, b.Height)); } // /// /// /// If the window closed with an button, returns its result. Else returns 0. /// Note: if the button is in a tab page, use the variable of that page. /// public int ResultButton => _resultButton; int _resultButton; /// /// Adds OK and/or Cancel and/or Apply buttons. /// /// Text of OK button. If null, does not add the button. /// Text of Cancel button. If null, does not add the button. /// Text of Apply button. If null, does not add the button. /// Add a right-bottom aligned that contains the buttons. See . If null (default), adds if not already in a stack panel, except when there is 1 button. /// /// Sets properties of OK/Cancel buttons so that click and Enter/Esc close the window; then returns true on OK, false on Cancel. /// See also event . /// public wpfBuilder AddOkCancel(string ok = "OK", string cancel = "Cancel", string apply = null, bool? stackPanel = null) => AddOkCancel(out _, out _, out _, ok, cancel, apply, stackPanel); /// Variable of OK button. /// Variable of Cancel button. /// Variable of Apply button. /// public wpfBuilder AddOkCancel(out Button bOK, out Button bCancel, out Button bApply, string ok = "OK", string cancel = "Cancel", string apply = null, bool? stackPanel = null) { int n = 0; if (ok != null) n++; if (cancel != null) n++; if (n == 0) throw new ArgumentNullException(); bool stack = stackPanel ?? (n > 1 && !(_p is _StackPanel)); if (stack) StartOkCancel(); if (ok != null) AddButton(out bOK, ok, null, WBBFlags.OK); else bOK = null; if (cancel != null) AddButton(out bCancel, cancel, null, WBBFlags.Cancel); else bCancel = null; if (apply != null) AddButton(out bApply, apply, null, WBBFlags.Apply); else bApply = null; if (stack) End(); return this; } /// /// Adds control. /// /// If true, adds vertical separator. If false, horizontal. If null (default), adds vertical if in horizontal stack panel, else adds horizontal. /// /// In panel separator's default size is 1x1. Need to set size, like .AddSeparator().XY(0, 50, 100, 1). /// public wpfBuilder AddSeparator(bool? vertical = null) { Add(out Separator c); if (vertical ?? (_p.panel is StackPanel p && p.Orientation == Orientation.Horizontal)) { c.Style = _style_VertSep ??= c.FindResource(ToolBar.SeparatorStyleKey) as Style; } c.UseLayoutRounding = true; //workaround: separators of different thickness when high DPI return this; } Style _style_VertSep; /// /// Adds enum members as with checkboxes (if it's a [Flags] enum) or control. /// /// Variable for getting result later. See . /// Initial value. /// Enum members and their text/tooltip. Optional. Text can be: null, "text", "text|tooltip", "|tooltip". /// If not null, adds a or control with this label. If it's a [Flags] enum, adds as parent of checkboxes, else adds before the (uses 2 grid cells). /// Vertical stack. Default true. [EditorBrowsable(EditorBrowsableState.Never)] //obsolete. Not flexible enough (eg users may want Grid, not StackPanel), etc. Added EnumUI examples in cookbook. public wpfBuilder AddEnum(out EnumUI e, TEnum init = default, (TEnum value, string text)[] items = null, string label = null, bool vertical = true) where TEnum : unmanaged, Enum { if (typeof(TEnum).IsDefined(typeof(FlagsAttribute), false)) { if (label != null) StartStack(label, vertical: vertical); else StartStack(vertical: vertical); e = new EnumUI(Panel, init, items); End(); } else { ComboBox cb; if (label != null) Add(label, out cb); else Add(out cb); e = new EnumUI(cb, init, items); } return this; } /// /// Adds one or more empty cells in current row of current grid. /// /// Column count. /// In non-grid panel. /// /// Actually just changes column index where next element will be added. /// public wpfBuilder Skip(int span = 1) { if (span < 0) throw new ArgumentException(); var g = _p as _Grid ?? throw new InvalidOperationException("Skip() in non-grid panel"); g.Skip(span); return this; } /// /// Sets to add next element in the same grid cell as previous element. /// /// Width of next element. If negative - width of previous element. Also it adds to the corresponding margin of other element. If 0, simply adds in the same place as previous element. /// In non-grid panel or in a wrong place. /// /// Can be used to add 2 elements in 1 cell as a cheaper and more concise way than with a StartX function. /// Next element will inherit column index and span of previous element but won't inherit row span. /// /// /// /// public wpfBuilder And(double width) { var g = _p as _Grid ?? throw new InvalidOperationException("And() in non-grid panel"); g.And(width); return this; } #endregion #region set common properties of last added element /// /// Sets column span of the last added element. /// /// Column count. If -1 or too many, will span all remaining columns in current row. If 0, will share 1 column with next element added in current row; to set element positions use , and ; see also . /// In non-grid panel. public wpfBuilder Span(int columns) { _ParentOfLastAsOrThrow<_Grid>().Span(columns); return this; } /// /// Sets row span of the last added element. /// /// Row count. /// In non-grid panel. /// /// In next row(s) use to skip cells occupied by this element. /// Often it's better to add a nested panel instead. See . /// public wpfBuilder SpanRows(int rows) { var c = _ParentOfLastAsOrThrow<_Grid>().LastDirect; Grid.SetRowSpan(c, rows); return this; } //rejected ///// ///// Calls your callback function. ///// ///// //public wpfBuilder Also(Action action) { // action(this); // return this; //} /// /// Sets callback function to be called by AddX functions for each element added afterwards. Not called by StartX functions for panels. /// /// Callback function or null. /// /// { /// if(b.Last is CheckBox c) { c.IsChecked = true; b.Margin("t1 b1"); } /// }); /// ]]> /// public wpfBuilder AlsoAll(Action action) { _alsoAll = action; return this; } Action _alsoAll; WBAlsoAllArgs _alsoAllArgs; /// /// Sets width and height of the last added element. Optionally sets alignment. /// /// Width or/and min/max width. /// Height or/and min/max height. /// Horizontal alignment. If not null, calls . /// Vertical alignment. /// Invalid alignment string. public wpfBuilder Size(WBLength width, WBLength height, string alignX = null, string alignY = null) { var c = Last; width.ApplyTo(c, false); height.ApplyTo(c, true); if (alignX != null || alignY != null) Align(alignX, alignY); return this; } /// /// Sets width of the last added element. Optionally sets alignment. /// /// Width or/and min/max width. /// Horizontal alignment. If not null, calls . /// Invalid alignment string. public wpfBuilder Width(WBLength width, string alignX = null) { width.ApplyTo(Last, false); if (alignX != null) Align(alignX); return this; } /// /// Sets height of the last added element. Optionally sets alignment. /// /// Height or/and min/max height. /// Vertical alignment. If not null, calls . /// Invalid alignment string. public wpfBuilder Height(WBLength height, string alignY = null) { height.ApplyTo(Last, true); if (alignY != null) Align(null, alignY); return this; } /// /// Sets position of the last added element in panel. Optionally sets size. /// /// /// /// Width or/and min/max width. /// Height or/and min/max height. /// Current panel is not . /// /// Only in panel you can set position explicitly. In other panel types it is set automatically and can be adjusted with , , container's , etc. /// public wpfBuilder XY(double x, double y, WBLength? width = null, WBLength? height = null) { var c = _ParentOfLastAsOrThrow<_Canvas>().LastDirect; Canvas.SetLeft(c, x); Canvas.SetTop(c, y); width?.ApplyTo(c, false); height?.ApplyTo(c, true); return this; } /// /// Docks the last added element in . /// /// /// Current panel is not . public wpfBuilder Dock(Dock dock) { var c = _ParentOfLastAsOrThrow<_DockPanel>().LastDirect; DockPanel.SetDock(c, dock); return this; } /// /// Sets horizontal and/or vertical alignment of the last added element. /// /// Horizontal alignment. /// Vertical alignment. /// Current panel is . public wpfBuilder Align(HorizontalAlignment? x = null, VerticalAlignment? y = null) { var c = Last; if (c.Parent is Canvas) throw new InvalidOperationException("Align() in Canvas panel."); if (x != null) c.HorizontalAlignment = x.Value; if (y != null) c.VerticalAlignment = y.Value; return this; } /// /// Sets horizontal and/or vertical alignment of the last added element. /// /// Horizontal alignment. String that starts with one of these letters, uppercase or lowercase: L (left), R (right), C (center), S (stretch). /// Vertical alignment. String that starts with one of these letters, uppercase or lowercase: T (top), B (bottom), C (center), S (stretch). /// Current panel is . /// Invalid alignment string. public wpfBuilder Align(string x = null, string y = null) => Align(_AlignmentFromStringX(x), _AlignmentFromStringY(y)); HorizontalAlignment? _AlignmentFromStringX(string s, [CallerMemberName] string m_ = null) => s.NE() ? default(HorizontalAlignment?) : (char.ToUpperInvariant(s[0]) switch { 'L' => HorizontalAlignment.Left, 'C' => HorizontalAlignment.Center, 'R' => HorizontalAlignment.Right, 'S' => HorizontalAlignment.Stretch, _ => throw new ArgumentException(m_ + "(x)") }); VerticalAlignment? _AlignmentFromStringY(string s, [CallerMemberName] string m_ = null) => s.NE() ? default(VerticalAlignment?) : (char.ToUpperInvariant(s[0]) switch { 'T' => VerticalAlignment.Top, 'C' => VerticalAlignment.Center, 'B' => VerticalAlignment.Bottom, 'S' => VerticalAlignment.Stretch, _ => throw new ArgumentException(m_ + "(y)") }); /// /// Sets content alignment of the last added element. /// /// Horizontal alignment. /// Vertical alignment. /// The last added element is not . public wpfBuilder AlignContent(HorizontalAlignment? x = null, VerticalAlignment? y = null) { var c = _LastAsControlOrThrow(); if (x != null) c.HorizontalContentAlignment = x.Value; if (y != null) c.VerticalContentAlignment = y.Value; return this; } /// /// Sets content alignment of the last added element. /// /// Horizontal alignment. String like with . /// Vertical alignment. /// The last added element is not . /// Invalid alignment string. public wpfBuilder AlignContent(string x = null, string y = null) => AlignContent(_AlignmentFromStringX(x), _AlignmentFromStringY(y)); /// /// Sets margin of the last added element. /// public wpfBuilder Margin(Thickness margin) { Last.Margin = margin; return this; } /// /// Sets margin of the last added element. /// public wpfBuilder Margin(double? left = null, double? top = null, double? right = null, double? bottom = null) { var c = Last; var p = c.Margin; left ??= p.Left; top ??= p.Top; right ??= p.Right; bottom ??= p.Bottom; c.Margin = new(left.Value, top.Value, right.Value, bottom.Value); return this; } /// /// Sets margin of the last added element. /// /// /// String containing uppercase or lowercase letters for margin sides (L, T, R, B) optionally followed by a number (default 0) and optionally separated by spaces. Or just single number, to set all sides equal. /// Examples: "tb" (top 0, bottom 0), "L5 R15" (left 5, right 15), "2" (all sides 2). /// /// Invalid string. public wpfBuilder Margin(string margin) { var c = Last; var m = c.Margin; _ThicknessFromString(ref m, margin); c.Margin = m; return this; } static void _ThicknessFromString(ref Thickness t, string s, [CallerMemberName] string m_ = null) { if (s.NE()) return; if (s.ToInt(out int v1, 0, out int e1) && e1 == s.Length) { t = new(v1); return; } for (int i = 0; i < s.Length; i++) { var c = s[i]; if (c == ' ') continue; int v = s.ToInt(i + 1, out int end); if (end > 0) i = end - 1; //never mind: should be double. Currently we don't have a function that can recognize and convert part of string to double. switch (c) { case 't': case 'T': t.Top = v; break; case 'b': case 'B': t.Bottom = v; break; case 'l': case 'L': t.Left = v; break; case 'r': case 'R': t.Right = v; break; default: throw new ArgumentException(m_ + "()"); } } } /// /// Sets padding of the last added control. /// /// The last added element does not have Padding property. public wpfBuilder Padding(Thickness thickness) { new _PropGetSet(Last, "Padding").Set(thickness); return this; } /// /// Sets padding of the last added control. /// /// The last added element does not have Padding property. public wpfBuilder Padding(double? left = null, double? top = null, double? right = null, double? bottom = null) { var c = new _PropGetSet(Last, "Padding"); var p = c.Get; left ??= p.Left; top ??= p.Top; right ??= p.Right; bottom ??= p.Bottom; c.Set(new(left.Value, top.Value, right.Value, bottom.Value)); return this; } /// /// Sets padding of the last added control. /// /// /// String containing uppercase or lowercase letters for padding sides (L, T, R, B) optionally followed by a number (default 0) and optionally separated by spaces. Or just single number, to set all sides equal. /// Examples: "tb" (top 0, bottom 0), "L5 R15" (left 5, right 15), "2" (all sides 2). /// /// The last added element does not have Padding property. /// Invalid string. public wpfBuilder Padding(string padding) { var c = new _PropGetSet(Last, "Padding"); var p = c.Get; _ThicknessFromString(ref p, padding); c.Set(p); return this; } /// /// Sets of the last added element. /// /// If true (default), sets IsEnabled = false, else sets IsEnabled = true. public wpfBuilder Disabled(bool disabled = true) { Last.IsEnabled = !disabled; return this; } /// /// Sets of the last added element. /// /// If true (default), sets Hiden; if false - Visible; if null - Collapsed. public wpfBuilder Hidden(bool? hidden = true) { Last.Visibility = hidden switch { true => Visibility.Hidden, false => Visibility.Visible, _ => Visibility.Collapsed }; return this; } /// /// Sets tooltip text/content/object of the last added element. See . /// /// Tooltip text (string), or tooltip content element, or object. /// /// Text box with simple tooltip. /// /// Tooltip with content created by another . /// ().Image(icon.stock(StockIcon.INFO).ToWpfImage()) /// .R.Add().Text("Some ", "text", ".") /// .End(); /// //dialog /// var b = new wpfBuilder("Window").WinSize(300); /// b.R.AddButton("Example", null).Tooltip(btt.Panel); /// b.R.AddOkCancel(); /// b.End(); /// if (!b.ShowDialog()) return; /// ]]> /// public wpfBuilder Tooltip(object tooltip) { Last.ToolTip = tooltip; if (!_opt_showToolTipOnKeyboardFocus) ToolTipService.SetShowsToolTipOnKeyboardFocus(Last, false); return this; } //FUTURE: make easier to create tooltip content, eg Inlines of TextBlock. Would be good to create on demand. //FUTURE: hyperlinks in tooltip. Now does not work because tooltip closes when mouse leaves the element. /// /// Sets UI Automation name of the last added element. /// public wpfBuilder UiaName(string name) { Last.UiaSetName(name); return this; } /// /// Sets background and/or foreground brush (color, gradient, etc) of the last added element. /// /// Background brush. See , . Descendants usually inherit this property. /// Foreground brush. Usually sets text color. Descendants usually override this property. /// Last added element must be , , or . With foreground only or . /// /// ("Example1").Brush(Brushes.Cornsilk, Brushes.Green).Border(Brushes.BlueViolet, 1); /// b.R.Add /// public wpfBuilder Brush(Brush background = null, Brush foreground = null) { //named not Colors because: 1. Can set other brush than color, eg gradient. 2. Rarely used and in autocompletion lists is above Columns. var last = Last; if (foreground != null) { new _PropGetSet(Last, "Foreground").Set(foreground); } if (background != null) { if (last == _p.panel && !_IsNested && _window != null) last = _window; new _PropGetSet(Last, "Background").Set(background); } return this; } /// /// Sets background and/or foreground color of the last added element. /// /// Last added element must be , , or . With foreground only or . public wpfBuilder Brush(ColorInt? background = null, ColorInt? foreground = null) => Brush(background == null ? null : new SolidColorBrush((Color)background.Value), foreground == null ? null : new SolidColorBrush((Color)foreground.Value)); /// /// Sets border properties of the last added element, which can be or a -derived class. /// /// Border color brush. If null, uses . /// Border thickness. Ignored if thickness2 not null. /// Sets the Padding property. /// Sets . If used, the last added element must be . /// Border thickness to use instead of thickness. Allows to set non-uniform thickness. /// Last added element must be or . With cornerRadius only . /// /// ("Example1").Border(Brushes.BlueViolet, 1, new(5)).Brush(Brushes.Cornsilk, Brushes.Green); /// b.R.Add().Border(Brushes.Blue, 2, cornerRadius: 3).Child().Add /// public wpfBuilder Border(Brush color = null, double thickness = 1d, Thickness? padding = null, double? cornerRadius = null, Thickness? thickness2 = null) { color ??= SystemColors.ActiveBorderBrush; switch (Last) { case Control c: if (cornerRadius != null) throw new NotSupportedException("Border(): Last added must be Border, or cornerRadius null"); c.BorderBrush = color; c.BorderThickness = thickness2 ?? new(thickness); if (padding != null) c.Padding = padding.Value; break; case Border c: c.BorderBrush = color; c.BorderThickness = thickness2 ?? new(thickness); if (padding != null) c.Padding = padding.Value; if (cornerRadius != null) c.CornerRadius = new CornerRadius(cornerRadius.Value); break; default: throw new NotSupportedException("Border(): Last added must be Control or Border"); } return this; //tested: there are no other useful types that have these properties. } /// Border color. /// public wpfBuilder Border(ColorInt color, double thickness = 1d, Thickness? padding = null, double? cornerRadius = null, Thickness? thickness2 = null) => Border(new SolidColorBrush((Color)color), thickness, padding, cornerRadius, thickness2); /// /// Sets font properties of the last added element and its descendants. /// /// If not null, sets font name. Can be multiple fonts separated by commas. /// If not null, sets font size. /// If not null, sets font bold or not. /// If not null, sets font italic or not. public wpfBuilder Font(string name = null, double? size = null, bool? bold = null, bool? italic = null) { var c = Last; if (name != null) TextElement.SetFontFamily(c, new FontFamily(name)); if (size != null) TextElement.SetFontSize(c, size.Value); if (bold != null) TextElement.SetFontWeight(c, bold == true ? FontWeights.Bold : FontWeights.Normal); if (italic != null) TextElement.SetFontStyle(c, italic == true ? FontStyles.Italic : FontStyles.Normal); return this; //rejected: FontStretch? stretch=null. Rarely used. Most fonts don't support. //not sure is this is OK or should set font properties for each supporting class separately. } /// /// Sets TextWrapping property of the last added element. /// Supports , and . /// public wpfBuilder Wrap(TextWrapping wrapping) { switch (Last) { case TextBlock t: t.TextWrapping = wrapping; break; case TextBox t: t.TextWrapping = wrapping; break; case AccessText t: t.TextWrapping = wrapping; break; default: throw new NotSupportedException("Wrap(): Last added must be TextBlock, TextBox or AccessText"); } return this; } /// /// Sets TextWrapping property of the last added element = if true (default), else or . /// Supports , and . /// public wpfBuilder Wrap(bool wrap = true) => Wrap(wrap ? TextWrapping.Wrap : TextWrapping.NoWrap); /// /// Attempts to set focus to the last added element when it'll become visible. /// public wpfBuilder Focus() { Last.Focus(); return this; } /// /// Sets Tag property of the last added element. /// public wpfBuilder Tag(object tag) { Last.Tag = tag; return this; } /// /// Sets property of the last added element. /// Then with of this and descendant elements don't need to specify data source object because it is set by this function. /// /// Data source object. public wpfBuilder BindingContext(object source) { Last.DataContext = source; return this; } /// /// Calls of the last added element. /// /// Element's dependency property, for example TextBox.TextProperty. /// Source property name or path, for example nameof(MyData.Property). Source object should be set with . public wpfBuilder Bind(DependencyProperty property, string path) { Last.SetBinding(property, path); return this; } /// /// Calls of the last added element. /// /// Element's dependency property, for example TextBox.TextProperty. /// A binding object, for example new Binding(nameof(MyData.Property)) or new Binding(nameof(MyData.Property)) { Source = dataObject }. In the first case, source object should be set with . public wpfBuilder Bind(DependencyProperty property, BindingBase binding) { Last.SetBinding(property, binding); return this; } /// /// Calls of the last added element and gets its return value. /// /// Element's dependency property, for example TextBox.TextProperty. /// A binding object. /// The return value of SetBinding. public wpfBuilder Bind(DependencyProperty property, BindingBase binding, out BindingExpressionBase r) { r = Last.SetBinding(property, binding); return this; } /// /// Calls of the last added element. Creates that uses source and path. /// /// Element's dependency property, for example TextBox.TextProperty. /// Data source object. /// Source property name or path, for example nameof(MyData.Property). public wpfBuilder Bind(DependencyProperty property, object source, string path) { var binding = new Binding(path) { Source = source }; Last.SetBinding(property, binding); return this; } /// /// Sets a validation callback function for the last added element. /// /// Function that returns an error string if element's value is invalid, else returns null. /// If not null, called on error link click. For example can be used to make the element visible. /// /// The callback function will be called when clicked button OK or Apply or a button added with flag . /// If it returns a non-null string, the window stays open and button's click callback not called. The string is displayed in a tooltip. /// /// /// string.IsNullOrWhiteSpace(tName.Text) ? "Name cannot be empty" : null); /// b.R.Add("Count", out TextBox tCount) /// .Validation(o => int.TryParse(tCount.Text, out int i1) && i1 >= 0 && i1 <= 100 ? null : "Count must be 0-100"); /// b.R.AddOkCancel(); /// b.End(); /// if (!b.ShowDialog()) return; /// print.it(tName.Text, tCount.Text.ToInt()); /// ]]> /// public wpfBuilder Validation(Func func, Action linkClick = null/*, DependencyProperty property=null*/) => Validation(Last, func, linkClick); /// /// Sets a validation callback function for an element. /// /// /// public wpfBuilder Validation(FrameworkElement e, Func func, Action linkClick = null/*, DependencyProperty property=null*/) { //validate on click of OK or some other button. Often eg text fields initially are empty and must be filled. (_validations ??= new List<_Validation>()).Add(new(e, func, linkClick)); return this; //rejected: also validate on lost focus or changed property value. } record class _Validation(FrameworkElement e, Func func, Action linkClick); List<_Validation> _validations; bool _Validate(Window w, Button b) { ToolTip tt = null; TextBlock tb = null; foreach (var gb in _GetAllWpfBuilders(w)) { //find all wpfBuilder used to build this window if (gb._validations == null) continue; foreach (var (e, func, linkClick) in gb._validations) { var s = func(e); if (s == null) continue; if (tb == null) tb = new TextBlock(); else tb.Inlines.Add(new LineBreak()); var h = new Hyperlink(new Run(s)); h.Click += (o, y) => { if (!e.IsVisible && _FindAncestorTabItem(e, out var ti)) ti.IsSelected = true; linkClick?.Invoke(e); if (e.IsVisible) { timer.after(1, _ => { //else does not focus etc if was in hidden tab page try { e.BringIntoView(); e.Focus(); _RedBorderAdorner.AddTo(e, true); tt.Closed += (_, _) => { _RedBorderAdorner.AddTo(e, false); }; } catch { } //catch(Exception e1) { print.it(e1); } }); } }; tb.Inlines.Add(h); } } if (tb == null) return true; tt = new ToolTip { Content = tb, StaysOpen = false, PlacementTarget = b, Placement = PlacementMode.Bottom }; //var tt=new Popup { Child=tb, StaysOpen=false, PlacementTarget=b, Placement= PlacementMode.Bottom }; //works, but black etc tt.IsOpen = true; //never mind: could add eg red rectangle, like WPF does on binding validation error. Not easy. return false; } static List _GetAllWpfBuilders(DependencyObject root) { var a = new List(); _Enum(root, 0); void _Enum(DependencyObject parent, int level) { foreach (var o in LogicalTreeHelper.GetChildren(parent).OfType()) { //print.it(new string(' ', level) + o); if (o is Panel p && s_cwt.TryGetValue(p, out var gb)) a.Add(gb); _Enum(o, level + 1); } } return a; } static bool _FindAncestorTabItem(DependencyObject e, out TabItem ti) { ti = null; for (; ; ) { switch (e = LogicalTreeHelper.GetParent(e)) { case null: return false; case TabItem t: ti = t; return true; } } } class _RedBorderAdorner : Adorner { public _RedBorderAdorner(UIElement adornedElement) : base(adornedElement) { } protected override void OnRender(DrawingContext drawingContext) { var adornedElementRect = new Rect(AdornedElement.RenderSize); var pen = new Pen(Brushes.Red, 2); drawingContext.DrawRectangle(null, pen, adornedElementRect); } public static void AddTo(FrameworkElement e, bool add) { if (AdornerLayer.GetAdornerLayer(e) is { } layer) { if (add) { layer.Add(new _RedBorderAdorner(e)); } else { if (layer.GetAdorners(e)?.OfType<_RedBorderAdorner>().FirstOrDefault() is { } k) layer.Remove(k); } } } } /// /// Sets of the last added element. /// /// Name. Must start with a letter or _, and contain only letters, digits and _. /// Also set UI Automation name (). /// Invalid name. /// /// The Name property can be used to identify the element in code. It also sets the UIA AutomationId (regardless of andUia). It isn't displayed in UI. /// public wpfBuilder Name(string name, bool andUia = false) { Last.Name = name; if (andUia) UiaName(name); return this; } /// /// Makes an element behave as a label of the last added element (). /// /// The label element. Usually or , but can be any element. /// If true, binds Visibility of label to that of the labeled element. If false, does not bind. If null (default), binds if the bindLabelVisibility option is true (see ). If binds Visibility, also binds IsEnabled if label is or . /// /// Sets label's if it's . Calls . /// public wpfBuilder LabeledBy(FrameworkElement label, bool? bindVisibility = null) { var e = Last; System.Windows.Automation.AutomationProperties.SetLabeledBy(e, label); if (label is Label la) la.Target = e; if (bindVisibility ?? _opt_bindLabelVisibility) { label?.SetBinding(UIElement.VisibilityProperty, new Binding("Visibility") { Source = e, Mode = BindingMode.OneWay }); if (label is Label or TextBlock) label.SetBinding(UIElement.IsEnabledProperty, new Binding("IsEnabled") { Source = e, Mode = BindingMode.OneWay }); } return this; } /// /// Makes behave as a label of . /// /// public wpfBuilder LabeledBy(bool? bindVisibility = null) => LabeledBy(Last2, bindVisibility); /// /// Sets watermark/hint/cue text of the last added , or editable control. /// The text is visible only when the control text is empty. /// /// Watermark text. /// /// In some kinds of windows it may not work unless a parent or ancestor of the control is an (like in the example). /// /// The last added element isn't , or editable control. /// /// ().Child().Add(out TextBox text1).Watermark("Water"); /// ]]> /// More examples in Cookbook. /// public wpfBuilder Watermark(string text) => Watermark(out _, text); /// Receives the adorner. It can be used to change watermark text later. /// public wpfBuilder Watermark(out WatermarkAdorner adorner, string text) { var c = Last as Control; if (!(c is TextBox or PasswordBox or ComboBox { IsEditable: true })) throw new NotSupportedException("Watermark(): Last added must be TextBox, PasswordBox or editable ComboBox"); adorner = new WatermarkAdorner(c, text); adorner.SetAdornerVisibility(); return this; } #endregion #region set type-specific properties of last added element /// /// Sets and of the last added check box or radio button. /// /// /// /// The last added element is not . public wpfBuilder Checked(bool? check = true, bool threeState = false) { var c = Last as ToggleButton ?? throw new NotSupportedException("Checked(): Last added element must be CheckBox or RadioButton"); c.IsThreeState = threeState; c.IsChecked = check; return this; } /// /// Sets of the specified . /// /// /// /// /// Unlike other similar functions, does not use . /// public wpfBuilder Checked(bool check, RadioButton control) { control.IsChecked = check; return this; } /// /// Sets or of the last added text box or editable combo box. /// /// /// Sets . Not used with . /// The last added element is not or . public wpfBuilder Readonly(bool readOnly = true, bool caretVisible = false) { //rejected: , bool caretVisible=false. Not useful. switch (Last) { case TextBoxBase c: c.IsReadOnly = readOnly; c.IsReadOnlyCaretVisible = caretVisible; break; case ComboBox c: c.IsReadOnly = readOnly; break; default: throw new NotSupportedException("Readonly(): Last added must be TextBox, RichTextBox or ComboBox"); } return this; } /// /// Makes the last added multiline. /// /// If not null, sets height or/and min/max height. /// Sets . /// The last added element is not . public wpfBuilder Multiline(WBLength? height = null, TextWrapping wrap = TextWrapping.WrapWithOverflow) { var c = Last as TextBox ?? throw new NotSupportedException("Multiline(): Last added must be TextBox"); c.AcceptsReturn = true; c.TextWrapping = wrap; c.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; c.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; height?.ApplyTo(c, true); return this; } /// /// Makes the last added editable. /// /// The last added element is not . public wpfBuilder Editable() { var c = Last as ComboBox ?? throw new NotSupportedException("Editable(): Last added must be ComboBox"); c.IsEditable = true; if (_ModifyPadding == true) c.Padding = new(2, 1, 2, 2); //default (2) or set by _Add() for non-editable return this; } /// /// Splits string and ads substrings as items to the last added (, etc). /// /// String like "One|Two|Three". /// The last added element is not . /// /// If it is a non-editable , selects the first item. See also . /// public wpfBuilder Items(string items) => _Items(items.Split('|'), null); /// /// Adds items of any type to the last added (, etc). /// /// Items of any type (string, WPF element). /// The last added element is not . /// /// If it is a non-editable , selects the first item. See also . /// public wpfBuilder Items(params object[] items) => _Items(items, null); wpfBuilder _Items(object[] a, IEnumerable e) { var ic = Last as ItemsControl ?? throw new NotSupportedException("Items(): Last added must be ItemsControl, for example ComboBox"); ic.ItemsSource = null; if (a != null) { ic.Items.Clear(); foreach (var v in a) ic.Items.Add(v); } else if (e != null) { ic.ItemsSource = e; } if (Last is ComboBox cb && !cb.IsEditable && cb.HasItems) cb.SelectedIndex = 0; return this; } /// /// Adds items as to the last added (, etc), with "lazy" option. /// /// An that contains items (eg array, ) or generates items (eg returned from a yield-return function). /// Retrieve items when (if) showing the dropdown part of the first time. /// /// - The last added element is not . /// - lazy is true and the last added element is not . /// public wpfBuilder Items(IEnumerable items, bool lazy = false) => lazy ? Items(true, o => o.ItemsSource = items) : _Items(null, items); /// /// Sets callback function that should add items to the last added later. /// /// Call the function once. If false, calls on each drop down. /// Callback function that should add items. Called before showing the dropdown part of the . Don't need to clear old items. /// The last added element is not . public wpfBuilder Items(bool once, Action onDropDown) { var c = Last as ComboBox ?? throw new NotSupportedException("Items(): Last added must be ComboBox"); EventHandler d = null; d = (_, _) => { if (once) c.DropDownOpened -= d; if (c.ItemsSource != null) c.ItemsSource = null; else c.Items.Clear(); onDropDown(c); }; c.DropDownOpened += d; return this; } /// /// Selects an item of the last added (, etc). /// /// 0-based item index /// The last added element is not . /// public wpfBuilder Select(int index) { var c = Last as Selector ?? throw new NotSupportedException("Items(): Last added must be Selector, for example ComboBox or ListBox"); c.SelectedIndex = index; return this; } /// /// Selects an item of the last added (, etc). /// /// An added item. /// The last added element is not . /// public wpfBuilder Select(object item) { var c = Last as Selector ?? throw new NotSupportedException("Items(): Last added must be Selector, for example ComboBox or ListBox"); c.SelectedItem = item; return this; } /// /// Obsolete. Use . /// Adds inlines to the last added . /// /// /// Arguments of type: ///
• string like "<b>text", "<i>text" or "<u>text" adds inline of type Bold, Italic or Underline. ///
• string like "<a>text" adds . Next argument of type Action or Action<Hyperlink> sets its action. ///
• other string - plain text. ///
adds a hyperlink. ///
of any type, eg Run, Bold, Hyperlink. ///
. /// /// The last added element is not TextBlock. /// Unsupported argument type. /// /// ().Text( /// "Text ", "bold ", "\n", /// new WBLink("libreautomate", "https://www.libreautomate.com"), ", ", /// new WBLink("Action", _ => print.it("click"), bold: true), "\n", /// new Run("color") { Foreground = Brushes.Blue, Background = Brushes.Cornsilk, FontSize = 20 }, "\n", /// "controls", new TextBox() { MinWidth = 100, Height = 20, Margin = new(3) }, new CheckBox() { Content = "Check" }, "\n", /// "image", ImageUtil.LoadWpfImageElement("*PixelartIcons.Notes #0060F0") /// ); /// ]]> /// [EditorBrowsable(EditorBrowsableState.Never)] //obsolete public wpfBuilder Text(params object[] inlines) { var c = Last as TextBlock ?? throw new NotSupportedException("Text(): Last added must be TextBlock"); var k = c.Inlines; k.Clear(); Hyperlink link = null; foreach (var v in inlines) { Inline n = null; int i; switch (v) { case WBLink x: n = x.Hlink; break; case Hyperlink x: n = link = x; break; case Inline x: n = x; break; case Action x when link != null: // fbc link.Click += (o, e) => x(); continue; case Action x when link != null: // fbc link.Click += (o, e) => x(o as Hyperlink); continue; case UIElement x: n = new InlineUIContainer(x) { BaselineAlignment = BaselineAlignment.Center }; break; case string x: if (x.Starts('<') && (i = x.Starts(false, "", "", "", "")) > 0) { var run = new Run(x[3..]); switch (i) { case 1: n = link = new Hyperlink(run); break; case 2: n = new Bold(run); break; case 3: n = new Italic(run); break; case 4: n = new Underline(run); break; } } else { k.Add(x); continue; } break; default: throw new ArgumentException("Text(): unsupported argument type"); } k.Add(n); } return this; } /// /// Adds inlines (text, formatted text, hyperlinks, images, etc) to the last added etc. /// /// /// Interpolated string (like $"string") with tags etc. The format is XML without root element. /// /// These tags add inlines of these types: ///
<b>text</b> - . ///
<i>text</i> - . ///
<u>text</u> - . ///
<s>text</s> - . ///
<s {Span}>text</s> - or a -based type. The function adds text to its Inlines collection. ///
<a {Action or Action<Hyperlink>}>text</a> - that calls the action. ///
<a href='URL or path etc'>text</a> - that calls . ///
/// /// Tags can have these attributes, like <s c='red' FontSize = '20'>text</s>: ///
c or Foreground - text color, like 'red' or '#AARRGGBB' or '#RRGGBB' or '#ARGB' or '#RGB'. ///
b or Background - background color. ///
FontFamily - font name, like 'Consolas'. ///
FontSize - font size, like '20'. ///
ToolTip - tooltip text. ///
/// /// WPF elements of these types can be inserted without tags: ///
{Inline} - any inline, eg . See also <s {Span}>text</s>. ///
{UIElement} - a WPF element, eg or . ///
{ImageSource} - adds . ///
{IEnumerable<Inline>} - adds multiple . ///
/// XML special characters must be escaped: ///
< - &lt;. ///
& - &amp;. ///
', " - &apos; in 'attribute' or &quot; in "attribute". /// /// /// The text in above examples can contain nested tags and elements. /// The <a> tag creates an image hyperlink if text is {image}, where image is a with image (see ) or (see , ). /// /// Unsupported type of the last added element. Or supported type but non-empty Content and Header (read Remarks). /// Unknown <tag> or unsupported {object} type. /// The same {Span} or {Inline} object in multiple places. /// Invalid color attribute. /// Exceptions of . /// /// The last added element can be of type: ///
- the function adds inlines to its Inlines collection. ///
(eg or ) - creates new with inlines and sets its Content property if it is null. If (eg ) and its Header property is null, sets Header instead. ///
whose Parent is (eg b.StartGrid<GroupBox>(null).FormatText($"...")) - uses the like the above. /// /// For elements other than the last added use or . /// /// To load images can be used and . ///
/// /// ().FormatText($""" /// Text bold italic underline. /// attributes /// Span object, bold ///
example.com Notepad /// { print.it("click"); }}>click { print.it("click once"); h.IsEnabled = false; }}>click once /// { print.it("click"); }} ToolTip='image hyperlink'>{ImageUtil.LoadWpfImageElement("*Entypo.HelpWithCircle #008EEE @14")} /// {new Run("Run object") { Foreground = Brushes.Blue, Background = Brushes.Goldenrod, FontSize = 20 }} /// Image {ImageUtil.LoadWpfImageElement("*PixelartIcons.Notes #0060F0")} /// Controls {new TextBox() { MinWidth = 100, Height = 20, Margin = new(3) }} {new CheckBox() { Content = "Check" }} /// """); /// ]]> /// Build interpolated string at run time. /// bold { print.it("click"); }); /// s.AppendLiteral(">link."); /// b.R.Add().FormatText(s); /// ]]> ///
public wpfBuilder FormatText(InterpolatedString text) { var s = text.GetFormattedText(); _FormatText(Last, s, text.aObj); return this; } //rejected: overload `FormatText(string text)` that supports only tags where don't need {object}. /// /// Adds inlines (text, formatted text, hyperlinks, images, etc) to the specified etc. /// /// Object of type , or . More info in remarks. /// Unsupported obj type or non-empty Content/Header. /// Unknown <tag> or unsupported {object} type. /// The same {Span} or {Inline} object in multiple places. /// Invalid color attribute. /// Exceptions of . /// public static void formatTextOf(object obj, InterpolatedString text) { var s = text.GetFormattedText(); _FormatText(obj, s, text.aObj); } /// /// Creates new and adds inlines like . /// /// Unknown <tag> or unsupported {object} type. /// The same {Span} or {Inline} object in multiple places. /// Invalid color attribute. /// Exceptions of . /// /// /// Label"), out TextBox _); /// b.R.AddButton(_TextWithIcon("Button", "*PixelartIcons.Notes #0060F0"), _ => { print.it("Button clicked"); }); /// /// static TextBlock _TextWithIcon(string text, string icon) { /// var e = ImageUtil.LoadWpfImageElement(icon); /// e.Margin = new(0, 0, 4, 0); /// return wpfBuilder.formattedText($"{e}{text}"); /// } /// ]]> /// public static TextBlock formattedText(InterpolatedString text) { var e = new TextBlock(); var s = text.GetFormattedText(); _FormatText(e, s, text.aObj); return e; } static void _FormatText(object obj, string text, List a) { //print.it(text, a); InlineCollection ic; g1: switch (obj) { case TextBlock k: ic = k.Inlines; break; case HeaderedContentControl k: k.Header = obj = new TextBlock(); goto g1; case ContentControl k: k.Content = obj = new TextBlock(); goto g1; case Panel k when k.Parent is HeaderedContentControl p1: obj = p1; goto g1; //eg b.StartGrid(null).FormatText($"...") case InlineCollection k: ic = k; break; default: throw new NotSupportedException("Format(): unsupported element type"); } ic.Clear(); var xr = XElement.Parse("" + text + "", LoadOptions.PreserveWhitespace); _Enum(ic, xr); void _Enum(InlineCollection ic, XElement xp) { for (var xn = xp.FirstNode; xn != null; xn = xn.NextNode) { if (xn is XElement xe) _Element(xe); else if (xn is XText xt) _Text(xt); //also XCData } void _Element(XElement x) { Span r = null; Hyperlink h = null; var tag = x.Name.LocalName; switch (tag) { case "b": r = new Bold(); break; case "i": r = new Italic(); break; case "u": r = new Underline(); break; case "s": if (_GetAttrObj() is object o) { //... r = o as Span ?? throw new ArgumentException("Expected "); if (r.Parent != null) throw new InvalidOperationException("Reused {Span} object"); } else { //... r = new(); } break; case "a": h = new Hyperlink(); r = h; if (x.Attr("href") is string href) { // h.Click += (_, _) => run.itSafe(href); } else { // switch (_GetAttrObj()) { case Action g: h.Click += (_, _) => g(); break; case Action g: h.Click += (_, _) => g(h); break; default: throw new ArgumentException("Expected or "); } } break; default: throw new ArgumentException($"Unknown tag <{tag}>"); } foreach (var at in x.Attributes()) { var v = at.Value; switch (at.Name.LocalName) { case "c" or "Foreground": r.Foreground = _Brush(v); break; case "b" or "Background": r.Background = _Brush(v); break; case "FontFamily": r.FontFamily = new(v); break; case "FontSize": if (v.ToNumber(out double fsize)) r.FontSize = fsize; break; case "ToolTip": r.ToolTip = v; break; } } ic.Add(r); if (h == null || !_HyperlinkImage(x, h)) _Enum(r.Inlines, x); object _GetAttrObj() => a != null && x.Attr("_a") is string s && s.Starts(c_mark2) && s.ToInt(out int i1, c_mark2.Length) ? a[i1] : null; static Brush _Brush(string v) => new SolidColorBrush((Color)ColorConverter.ConvertFromString(v)); } void _Text(XText x) { var s = x.Value; if (a != null) { int from = 0; for (; from < s.Length; from++) { int m = s.Find(c_mark, from); if (m < 0) break; if (m > from) ic.Add(s[from..m]); if (s.ToInt(out int i, m + c_mark.Length, out from)) { var k = a[i]; g2: switch (k) { case Inline e: if (e.Parent != null) throw new InvalidOperationException("Reused {Inline} object"); ic.Add(e); break; case UIElement e: ic.Add(new InlineUIContainer(e) { BaselineAlignment = BaselineAlignment.Center }); break; case ImageSource e: k = new Image { Source = e, Stretch = Stretch.None }; goto g2; case IEnumerable e: ic.AddRange(e); break; default: throw new ArgumentException($"Unexpected element type {a[i]}"); } } } if (from < s.Length) ic.Add(s[from..]); } else ic.Add(s); } } bool _HyperlinkImage(XElement x, Hyperlink h) { if (x.FirstNode is XText xt && xt.NextNode == null && xt.Value is string s && s.Starts(c_mark) && s.ToInt(out int i, c_mark.Length, out int end) && end == s.Length - 1) { UIElement eImg = a[i] switch { UIElement e => e, ImageSource e => new Image { Source = e, Stretch = Stretch.None }, _ => null }; if (eImg == null) return false; Border border = new() { //workaround for: image's transparent areas are not hittest-sensitive. InlineUIContainer.Background does not fix it. Child = eImg, Background = Brushes.Transparent, Padding = new(0, 2, 0, 0) }; InlineUIContainer uc = new(border) { BaselineAlignment = BaselineAlignment.Center }; h.Inlines.Add(uc); h.TextDecorations = null; return true; } return false; } } const string c_mark = "_a='≡∫∫≡", c_mark2 = "≡∫∫≡"; //start of mark. Full mark is like "_a='≡∫∫≡0'", where 0 is aObj index. With this format, can be used as an XML attribute, as well as in XML text. #pragma warning disable 1591 //no XML doc [InterpolatedStringHandler, NoDoc] public ref struct InterpolatedString { DefaultInterpolatedStringHandler _f; internal List aObj; public InterpolatedString(int literalLength, int formattedCount) { _f = new(literalLength, formattedCount); } public InterpolatedString(int literalLength, int formattedCount, IFormatProvider provider) { _f = new(literalLength, formattedCount, provider); } public InterpolatedString(int literalLength, int formattedCount, IFormatProvider provider, Span initialBuffer) { _f = new(literalLength, formattedCount, provider, initialBuffer); } public void AppendLiteral(string value) => _f.AppendLiteral(value); public void AppendFormatted(string value) => _f.AppendFormatted(value); public void AppendFormatted(T value) { if (value is Delegate or DependencyObject or IEnumerable) { aObj ??= new(); _f.AppendLiteral(c_mark); _f.AppendLiteral(aObj.Count.ToS()); _f.AppendLiteral("'"); aObj.Add(value); } else _f.AppendFormatted(value); } public void AppendFormatted(T value, int alignment) => _f.AppendFormatted(value, alignment); public void AppendFormatted(T value, string format) { _f.AppendFormatted(value, format); } public void AppendFormatted(T value, int alignment, string format) { _f.AppendFormatted(value, alignment, format); } public void AppendFormatted(RStr value) => _f.AppendFormatted(value); public void AppendFormatted(RStr value, int alignment = 0, string format = null) => _f.AppendFormatted(value, alignment, format); public void AppendFormatted(string value, int alignment = 0, string format = null) => _f.AppendFormatted(value, alignment, format); public void AppendFormatted(object value, int alignment = 0, string format = null) => _f.AppendFormatted(value, alignment, format); public string GetFormattedText() => _f.ToStringAndClear(); } #pragma warning restore 1591 /// /// Loads a web page or RTF text from a file or URL into the last added element. /// /// File or URL to load. Supported element types and sources: /// , - URL or file path. /// - path of a local .rtf file. /// /// /// - Unsupported element type. /// - source does not end with ".rtf". /// /// /// If fails to load, prints warning. See . /// public wpfBuilder LoadFile(string source) { var c = Last; bool bad = false; try { source = _UriNormalize(source); switch (c) { case WebBrowser u: u.Source = new Uri(source); break; case Frame u: u.Source = new Uri(source); break; case RichTextBox u when source.Ends(".rtf", true): using (var fs = File.OpenRead(source)) { u.Selection.Load(fs, DataFormats.Rtf); } //also supports DataFormats.Text,Xaml,XamlPackage. If need HTML, download and try HtmlToXamlConverter. See https://www.codeproject.com/Articles/1097390/Displaying-HTML-in-a-WPF-RichTextBox break; default: bad = true; break; } } catch (Exception ex) { print.warning("LoadFile() failed. " + ex.ToString(), -1); } if (bad) throw new NotSupportedException("LoadFile(): Unsupported type of element or source."); return this; } /// /// Loads image into the last added . /// /// Sets . /// Sets . /// Sets . /// The last added element is not . /// /// To load vector images from XAML, don't use control and this function. Instead create control from XAML, for example with , and add it with . /// /// /// public wpfBuilder Image(ImageSource source, Stretch stretch = Stretch.None, StretchDirection stretchDirection = StretchDirection.DownOnly) => _Image(source, null, stretch, stretchDirection); wpfBuilder _Image(ImageSource source, string file, Stretch stretch, StretchDirection stretchDirection) { var c = Last as Image ?? throw new NotSupportedException("Image(): Last added must be Image"); if (file != null) { //try { source = new BitmapImage(_Uri(file)); } try { source = ImageUtil.LoadWpfImage(file); } catch (Exception ex) { print.warning("Image() failed. " + ex.ToString(), -1); } } c.Stretch = stretch; //default Uniform c.StretchDirection = stretchDirection; //default Both c.Source = source; return this; } /// /// Loads image from a file or URL into the last added . /// /// File path etc. See . Sets . /// Sets . /// Sets . /// The last added element is not . /// /// If fails to load, prints warning. See . /// public wpfBuilder Image(string source, Stretch stretch = Stretch.None, StretchDirection stretchDirection = StretchDirection.DownOnly) => _Image(null, source, stretch, stretchDirection); /// /// Sets vertical or horizontal splitter properties of the last added . /// /// If true, resizes columns, else rows. /// How many rows spans vertical splitter, or how many columns spans horizontal splitter. Can be more than row/column count. /// Width of vertical splitter or height of horizontal. If double.NaN, sets alignment "stretch", else "center". /// The last added element is not . /// /// Vertical splitter. /// ().Splitter(true, 2).Brush(Brushes.Orange) //add splitter in the middle column /// .Add(out TextBox _) /// .R.Add(out TextBox _).Skip().Add(out TextBox _) //skip the splitter's column /// .R.AddOkCancel() /// .End(); /// if (!b.ShowDialog()) return; /// ]]> /// Horizontal splitter. /// ().Splitter(false, 2).Brush(Brushes.Orange) /// .Row(-1).Add("Row", out TextBox _) /// .R.AddOkCancel() /// .End(); /// if (!b.ShowDialog()) return; /// ]]> /// public wpfBuilder Splitter(bool vertical, int span = 1, double thickness = 4) { var g = _ParentOfLastAsOrThrow<_Grid>(); var c = Last as GridSplitter ?? throw new NotSupportedException("Splitter(): Last added must be GridSplitter"); if (vertical) { c.HorizontalAlignment = double.IsNaN(thickness) ? HorizontalAlignment.Stretch : HorizontalAlignment.Center; c.VerticalAlignment = VerticalAlignment.Stretch; c.ResizeDirection = GridResizeDirection.Columns; c.Width = thickness; if (span != 1) Grid.SetRowSpan(c, span); } else { c.HorizontalAlignment = HorizontalAlignment.Stretch; c.VerticalAlignment = double.IsNaN(thickness) ? VerticalAlignment.Stretch : VerticalAlignment.Center; c.ResizeDirection = GridResizeDirection.Rows; c.Height = thickness; if (span != 1) g.Span(span); } c.ResizeBehavior = GridResizeBehavior.PreviousAndNext; return this; } //FUTURE: need a numeric input control. This code is for WinForms NumericUpDown. // public wpfBuilder Number(decimal? value = null, decimal? min = null, decimal? max=null, decimal? increment=null, int? decimalPlaces=null, bool? thousandsSeparator=null, bool? hex =null) { // var c = Last as NumericUpDown ?? throw new NotSupportedException("Number(): Last added must be NumericUpDown"); // if(min!=null) c.Minimum=min.Value; // if(max!=null) c.Maximum=max.Value; // if(increment!=null) c.Increment=increment.Value; // if(decimalPlaces!=null) c.DecimalPlaces=decimalPlaces.Value; // if(thousandsSeparator!=null) c.ThousandsSeparator=thousandsSeparator.Value; // if(hex!=null) c.Hexadecimal=hex.Value; // if(value!=null) c.Value=value.Value; else c.Text=null; // return this; // } #endregion #region nested panel wpfBuilder _Start(_PanelBase p, bool childOfLast) { _p.BeforeAdd(childOfLast); _AddToParent(p.panel, childOfLast); _p = p; return this; } wpfBuilder _Start(_PanelBase p, out T container, object header) where T : HeaderedContentControl, new() { Add(out container, header); container.Content = p.panel; if (container is GroupBox) p.panel.Margin = new(0, 2, 0, 0); _p = p; return this; } /// /// Adds panel (table) that will contain elements added with etc. Finally call to return to current panel. /// /// Add as a child or content of the last added element (), which must be a (for example or ) or (for example ). /// /// How changes: after calling this function it is the grid (); after adding an element it is the element; finally, after calling it is the grid if childOfLast false, else its parent. The same with all StartX functions. /// public wpfBuilder StartGrid(bool childOfLast = false) => _Start(new _Grid(this), childOfLast); /// /// Adds a headered content control (, , etc) with child panel (table) that will contain elements added with etc. Finally call to return to current panel. /// /// Header text/content. /// /// How changes: after calling this function it is the grid (); after adding an element it is the element; finally, after calling it is the content control (grid's parent). The same with all StartX functions. /// /// /// ("Group"); /// ]]> /// public wpfBuilder StartGrid(object header) where T : HeaderedContentControl, new() => _Start(new _Grid(this), out T _, header); /// Receives the content control's variable. The function creates new control of the type. /// /// /// /// public wpfBuilder StartGrid(out T container, object header) where T : HeaderedContentControl, new() => _Start(new _Grid(this), out container, header); /// /// Adds panel that will contain elements added with etc. Finally call to return to current panel. /// /// /// /// For each added control call or use indexer like [x, y] or [x, y, width, height]. /// public wpfBuilder StartCanvas(bool childOfLast = false) => _Start(new _Canvas(this), childOfLast); /// /// Adds a headered content control (, , etc) with child panel that will contain elements added with etc. Finally call to return to current panel. /// /// Header text/content. public wpfBuilder StartCanvas(object header) where T : HeaderedContentControl, new() => _Start(new _Canvas(this), out T _, header); /// Receives the content control's variable. The function creates new control of the type. /// public wpfBuilder StartCanvas(out T container, object header) where T : HeaderedContentControl, new() => _Start(new _Canvas(this), out container, header); /// /// Adds panel that will contain elements added with etc. Finally call to return to current panel. /// /// /// /// For added elements call , maybe except for the last element that fills remaining space. /// public wpfBuilder StartDock(bool childOfLast = false) => _Start(new _DockPanel(this), childOfLast); /// /// Adds a headered content control (, , etc) with child panel that will contain elements added with etc. Finally call to return to current panel. /// /// Header text/content. public wpfBuilder StartDock(object header) where T : HeaderedContentControl, new() => _Start(new _DockPanel(this), out T _, header); /// Receives the content control's variable. The function creates new control of the type. /// public wpfBuilder StartDock(out T container, object header) where T : HeaderedContentControl, new() => _Start(new _DockPanel(this), out container, header); /// /// Adds panel that will contain elements added with etc. Finally call to return to current panel. /// /// /// public wpfBuilder StartStack(bool vertical = false, bool childOfLast = false) => _Start(new _StackPanel(this, vertical), childOfLast); /// /// Adds a headered content control (, , etc) with child panel that will contain elements added with etc. Finally call to return to current panel. /// /// Header text/content. /// public wpfBuilder StartStack(object header, bool vertical = false) where T : HeaderedContentControl, new() => _Start(new _StackPanel(this, vertical), out T _, header); /// Receives the content control's variable. The function creates new control of the type. /// public wpfBuilder StartStack(out T container, object header, bool vertical = false) where T : HeaderedContentControl, new() => _Start(new _StackPanel(this, vertical), out container, header); /// /// Adds panel of any type. It will contain elements added with etc. Finally call to return to current panel. /// /// New panel of any type (, , etc). For and this function sets some panel properties to make everything work like with other StartX functions. /// public wpfBuilder StartPanel(Panel panel, bool childOfLast = false) => _Start(_StartPanel(panel), childOfLast); /// /// Adds a headered content control (, , etc) with child panel of any type. The panel will contain elements added with etc. Finally call to return to current panel. /// /// New panel of any type (, , etc). For and this function sets some panel properties to make everything work like with other StartX functions. /// Any -based type. /// Header text/content. public wpfBuilder StartPanel(Panel panel, object header) where TContainer : HeaderedContentControl, new() => _Start(_StartPanel(panel), out TContainer _, header); _PanelBase _StartPanel(Panel panel) { return panel switch { null => throw new ArgumentNullException(), Grid g => new _Grid(this, g), Canvas c => new _Canvas(this, c), _ => new _PanelBase(this, panel) }; } /// /// Adds right-bottom-aligned horizontal stack panel () for adding OK, Cancel and more buttons. /// When don't need more buttons, use just . /// /// /// { }).Width(70).End(); /// ]]> /// public wpfBuilder StartOkCancel() { var pa = _p; StartStack(); if (pa is not _Canvas) { _p.panel.HorizontalAlignment = HorizontalAlignment.Right; _p.panel.VerticalAlignment = VerticalAlignment.Bottom; _p.panel.Margin = new(0, 2, 0, 0); } return this; } #endregion #region util bool _IsNested => _p.parent != null; bool _IsWindowEnded => _p.ended && _p.parent == null; Window _FindWindow(DependencyObject c) => _window ?? Window.GetWindow(c); //CONSIDER: support top-level HwndSource window Window _GetOrFindWindow() => _window ?? Window.GetWindow(_p.panel); void _ThrowIfNotWindow([CallerMemberName] string m_ = null) { if (_window == null) throw new InvalidOperationException(m_ + "(): Container is not Window"); } Control _LastAsControlOrThrow([CallerMemberName] string m_ = null) => (Last as Control) ?? throw new InvalidOperationException(m_ + "(): Last added element is not Control"); _PanelBase _ParentOfLast => ReferenceEquals(Last, _p.panel) ? _p.parent : _p; T _ParentOfLastAsOrThrow([CallerMemberName] string m_ = null) where T : _PanelBase { return _ParentOfLast is T t ? t : throw new InvalidOperationException($"{m_}() not in {typeof(T).Name[1..]} panel."); } // void _ThrowIfParentOfLastIs([CallerMemberName] string caller = null) where TControl : Panel { // if(Last.Parent is TControl) throw new InvalidOperationException($"{caller}() in {typeof(TControl).Name} panel."); // } static string _UriNormalize(string source) => pathname.normalize(source, flags: PNFlags.CanBeUrlOrShell); //static Uri _Uri(string source) => new Uri(_UriNormalize(source)); /// /// Detects whether the application or window is using a theme. /// static _Themed _GetThemed(wpfBuilder b = null) { var a = Application.Current; #if NET9_0_OR_GREATER if (a != null && a.ThemeMode != ThemeMode.None) return _Themed.Wpf; if (b?._GetOrFindWindow() is { } w && w.ThemeMode != ThemeMode.None) return _Themed.Wpf; #endif if (a != null && a.Resources.MergedDictionaries.Count > 0) return _Themed.Other; return 0; } enum _Themed : byte { None, Wpf, Other } _Themed _IsThemed => _isThemed ??= _GetThemed(this); _Themed? _isThemed; /// /// Gets or sets a property of an element of any type that has the property. /// struct _PropGetSet { FrameworkElement _e; PropertyInfo _p; public _PropGetSet(FrameworkElement e, string prop, [CallerMemberName] string m_ = null) { _e = e; _p = e.GetType().GetProperty(prop, typeof(T)) ?? throw new InvalidOperationException(m_ + $"(): Last added element does not have {prop} property"); } /// /// Like ctor but does not throw. /// /// false if failed. public static bool TryCreate(FrameworkElement e, string prop, out _PropGetSet r) { var p = e.GetType().GetProperty(prop, typeof(T)); if (p == null) { r = default; return false; } r = new() { _e = e, _p = p }; return true; } public T Get => (T)_p.GetValue(_e); public void Set(T value) { _p.SetValue(_e, value); } } #endregion } ================================================ FILE: Au/Input/RegisteredHotkey.cs ================================================ namespace Au.More; /// /// Registers a hotkey using API RegisterHotKey. Unregisters when disposing. /// /// /// Can be used as a lightweight alternative to hotkey triggers. /// /// The variable must be disposed, either explicitly (call or ) or with using. /// /// /// /// /// public struct RegisteredHotkey : IDisposable { wnd _w; int? _id; ///// The hotkey. //public KHotkey Hotkey { get; private set; } /// /// Registers a hotkey using API RegisterHotKey. /// /// false if failed. Supports . /// Hotkey id. Must be 0 to 0xBFFF or value returned by API GlobalAddAtom. It will be wParam of the WM_HOTKEY message. /// Hotkey. Can be: string like "Ctrl+Shift+Alt+Win+K", tuple (KMod, KKey), enum , enum Keys, struct . /// Window/form that will receive the WM_HOTKEY message. Must be of this thread. If default, the message must be retrieved in the message loop of this thread. /// Add flag MOD_NOREPEAT. /// Error in hotkey string. /// This variable already registered a hotkey. /// /// Fails if the hotkey is currently registered by this or another application or used by Windows. Also if F12. /// Most single-key and Shift+key hotkeys don't work when the active window has higher UAC integrity level (eg admin) than this process. Media keys may work. /// A single variable cannot register multiple hotkeys simultaneously. Use multiple variables, for example array. /// /// public bool Register(int id, [ParamString(PSFormat.Hotkey)] KHotkey hotkey, AnyWnd window = default, bool noRepeat = false) { if (_id != null) throw new InvalidOperationException("This variable already registered a hotkey. Use multiple variables or call Unregister."); var w = window.Hwnd; var (mod, key) = Normalize_(hotkey); if (noRepeat) mod |= Api.MOD_NOREPEAT; if (!Api.RegisterHotKey(w, id, mod, key)) return false; _w = w; _id = id; //Hotkey = hotkey; return true; } internal static (uint mod, KKey key) Normalize_(KHotkey hotkey) { var (mod, key) = hotkey; if (key == KKey.Pause && mod.Has(KMod.Ctrl)) key = KKey.Break; //if(key == KKey.NumPad5 && mod.Has(KMod.Shift)) key = KKey.Clear; //Shift+numpad don't work return (Math2.SwapBits((uint)mod, 0, 2, 1), key); } /// /// Unregisters the hotkey. /// /// /// Called implicitly when disposing this variable. /// Must be called from the same thread as when registering, and the window must be still alive. /// If fails, calls . /// public void Unregister() { if (_id != null) { if (!Api.UnregisterHotKey(_w, _id.Value)) { var es = lastError.message; print.warning($"Failed to unregister hotkey, id={_id.Value}. {es}"); return; } _id = null; _w = default; //Hotkey = default; } } /// /// Calls . /// public void Dispose() => Unregister(); //~RegisteredHotkey() => Unregister(); //makes no sense. Called from wrong thread and when the window is already destroyed. /// /// This message is posted to the window or to the thread's message loop. /// More info: WM_HOTKEY. /// public const int WM_HOTKEY = Api.WM_HOTKEY; } ================================================ FILE: Au/Input/clipboard.cs ================================================ namespace Au; /// /// Clipboard functions. Copy, paste, get and set clipboard text and other data. /// /// /// This class is similar to the .NET class, which uses OLE API, works only in STA threads and does not work well in automation scripts. This class uses non-OLE API and works well in automation scripts and any threads. /// /// To set/get clipboard data of non-text formats, use class ; to paste, use it with ; to copy (get from the active app), use it with . /// /// Don't copy/paste in windows of own thread. Call it from another thread. Example in . /// public static class clipboard { /// /// Clears the clipboard. /// /// Failed to open clipboard (after 10 s of wait/retry). public static void clear() { using (new OpenClipboard_(false)) EmptyClipboard_(); } internal static void EmptyClipboard_() { if (!Api.EmptyClipboard()) Debug.Assert(false); } /// /// Gets or sets clipboard text. /// /// null if there is no text. /// Failed to open clipboard (after 10 s of wait/retry) or set clipboard data. /// The set function failed to allocate memory. /// /// The get function calls . /// /// Gets/sets only data of text format. For other formats (files, HTML, image, etc) use class. /// public static string text { get => clipboardData.getText(); set { using (new OpenClipboard_(true)) { EmptyClipboard_(); if (value != null) clipboardData.SetText_(value); } } } //Sets text (string) or multi-format data (Data). Clipboard must be open. static void _SetClipboard(object data, bool renderLater) { switch (data) { case clipboardData d: d.SetOpenClipboard(renderLater); break; case string s: if (renderLater) Api.SetClipboardData(Api.CF_UNICODETEXT, default); else clipboardData.SetText_(s); break; } } /// /// Calls API SetClipboardData("Clipboard Viewer Ignore"). Clipboard must be open. /// Then clipboard manager/viewer/etc programs that are aware of this convention don't try to get our clipboard data while we are pasting. /// Tested apps that support it: Ditto, Clipdiary. Other 5 tested apps don't. Windows 10 Clipboard History doesn't. /// static void _SetClipboardData_ClipboardViewerIgnore() { Api.SetClipboardData(ClipFormats.ClipboardViewerIgnore, Api.GlobalAlloc(Api.GMEM_MOVEABLE | Api.GMEM_ZEROINIT, 1)); //tested: hMem cannot be default(IntPtr) or 0 bytes. } /// /// Gets the selected text from the focused app using the clipboard. /// /// Use Ctrl+X. /// /// Options. If null (default), uses . /// Uses , , . Does not use . /// /// Keys to use instead of Ctrl+C or Ctrl+X. Example: hotkey: "Ctrl+Shift+C". Overrides cut. /// Max time to wait until the focused app sets clipboard data, in milliseconds. If 0 (default), the timeout is 3000 ms. The function waits up to 10 times longer if the window is hung. /// Failed. Fails if there is no focused window or if it does not set clipboard data. /// /// /// Also can get file paths, as multiline text. /// /// Sends keys Ctrl+C, waits until the focused app sets clipboard data, gets it, finally restores clipboard data. /// Fails if the focused app does not set clipboard text or file paths, for example if there is no selected text/files. /// Works with console windows too, even if they don't support Ctrl+C. /// public static string copy(bool cut = false, OKey options = null, [ParamString(PSFormat.Hotkey)] KHotkey hotkey = default, int timeoutMS = 0) { return _Copy(cut, hotkey, options, timeoutMS); //rejected: 'format' parameter. Not useful. } /// /// Calls and handles exceptions. /// /// Returns false if failed. /// Receives the copied text. /// Max time to wait until the focused app sets clipboard data, in milliseconds. The function waits up to 10 times longer if the window is hung. /// Call . /// Call with text "Failed to copy text". /// public static bool tryCopy(out string text, int timeout, bool warning = false, bool osd = false, OKey options = null, [ParamString(PSFormat.Hotkey)] KHotkey hotkey = default) { try { text = _Copy(false, hotkey, options, Math.Max(timeout, 25)); return true; } catch (Exception e1) { if (warning) print.warning(e1); if (osd) osdText.showTransparentText("Failed to copy text"); text = null; return false; } } /// /// Calls and handles exceptions. /// /// Returns false if failed. /// Receives the copied text. /// Call . Default true. /// Call with text "Failed to copy text". Default true. /// [EditorBrowsable(EditorBrowsableState.Never)] //obsolete. Not optimal parameters. public static bool tryCopy(out string text, bool cut = false, OKey options = null, bool warning = true, bool osd = true, [ParamString(PSFormat.Hotkey)] KHotkey hotkey = default, int timeoutMS = 0) { try { text = _Copy(cut, hotkey, options, timeoutMS); return true; } catch (Exception e1) { if (warning) print.warning(e1); if (osd) osdText.showTransparentText("Failed to copy text"); text = null; return false; } } /// /// Gets data of any formats from the focused app using the clipboard and a callback function. /// /// The return value of callback. /// Callback function. It can get clipboard data of any formats. It can use any clipboard functions, for example or . Don't call copy/paste functions. /// /// Sends keys Ctrl+C, waits until the focused app sets clipboard data, calls the callback function that gets it, finally restores clipboard data. /// Fails if the focused app does not set clipboard data. /// Works with console windows too, even if they don't support Ctrl+C. /// /// /// clipboardData.getImage()); /// /// var (text, files) = clipboard.copyData(() => (clipboardData.getText(), clipboardData.getFiles())); /// ]]> /// /// public static T copyData(Func callback, bool cut = false, OKey options = null, [ParamString(PSFormat.Hotkey)] KHotkey hotkey = default, int timeoutMS = 0) { Not_.Null(callback); T r = default; _Copy(cut, hotkey, options, timeoutMS, () => { r = callback(); }); return r; } /// [EditorBrowsable(EditorBrowsableState.Never)] //obsolete public static void copyData(Action callback, bool cut = false, OKey options = null, KHotkey hotkey = default) { Not_.Null(callback); _Copy(cut, hotkey, options, 0, callback); } static string _Copy(bool cut, KHotkey hotkey, OKey options, int timeoutMS, Action callback = null) { string R = null; var optk = options ?? opt.key; bool restore = optk.RestoreClipboard; _ClipboardListener listener = null; _DisableClipboardHistory disableCH = default; var bi = new inputBlocker() { ResendBlockedKeys = true }; var oc = new OpenClipboard_(createOwner: true, noOpenNow: !restore); try { if (!optk.NoBlockInput) bi.Start(BIEvents.Keys); keys.Internal_.ReleaseModAndDisableModMenu(); disableCH.Disable(); //fast var save = new _SaveRestore(); if (restore) { save.Save(); oc.Close(false); //close clipboard; don't destroy our clipboard owner window } wnd wFocus = keys.Internal_.GetWndFocusedOrActive(requireFocus: true); listener = new _ClipboardListener(false, null, oc.WndClipOwner, wFocus); if (!Api.AddClipboardFormatListener(oc.WndClipOwner)) throw new AuException(); var ctrlC = new keys.Internal_.SendCopyPaste(); try { if (wFocus.IsConsole) { wFocus.Post(Api.WM_SYSCOMMAND, 65520); //system menu &Edit > &Copy; tested on all OS; Windows 10 supports Ctrl+C, but it may be disabled. } else { if (hotkey.Key == 0) hotkey = new(KMod.Ctrl, cut ? KKey.X : KKey.C); ctrlC.Press(hotkey, optk, wFocus); } //wait until the app sets clipboard text listener.Wait(ref ctrlC, timeoutMS); } finally { ctrlC.Release(); Api.RemoveClipboardFormatListener(oc.WndClipOwner); } wFocus.SendTimeout(500, out _, 0); //workaround: in SharpDevelop and ILSpy (both WPF), API GetClipboardData takes ~1 s. Need to sleep min 10 ms or send message. if (callback != null) { callback(); if (restore) oc.Reopen(); } else { oc.Reopen(); R = clipboardData.GetText_(0); } if (restore) save.Restore(); } finally { oc.Dispose(); bi.Dispose(); disableCH.Restore(); } GC.KeepAlive(listener); if (R == null && callback == null) throw new AuException("*copy text"); //no text in the clipboard. Probably not a text control; if text control but empty selection, usually throws in Wait, not here, because the target app then does not use the clipboard. return R; } /// /// Pastes text or HTML into the focused app using the clipboard. /// /// Text. Can be null if html used. /// /// HTML. Can be full HTML or fragment. See . Can be null. /// Can be specified only text or only html or both. If both, will paste html in apps that support it, elsewhere text. If only html, in apps that don't support HTML will paste html as text. /// /// /// Options. If null (default), uses . /// Uses , , , , , . /// /// Keys to use instead of Ctrl+V. Example: hotkey: "Ctrl+Shift+V". /// Max time to wait until the focused app gets clipboard data, in milliseconds. If 0 (default), the timeout is 3000 ms. The function waits up to 10 times longer if the window is hung. /// Failed. Fails if there is no focused window or if it does not get clipboard data. /// /// /// Sets clipboard data, sends keys Ctrl+V, waits until the focused app gets clipboard data, finally restores clipboard data. /// Fails if nothing gets clipboard data in several seconds. /// Works with console windows too, even if they don't support Ctrl+V. /// /// A clipboard viewer/manager program can make this function slower and less reliable, unless it supports or gets clipboard data with a delay. /// Possible problems with some virtual PC programs. Either pasting does not work in their windows, or they use a hidden clipboard viewer that makes this function slower and less reliable. /// /// /// /// /// public static void paste(string text, string html = null, OKey options = null, [ParamString(PSFormat.Hotkey)] KHotkey hotkey = default, int timeoutMS = 0) { if (text.NE() && html.NE()) return; object data = text; if (html != null) data = new clipboardData().AddHtml(html).AddText(text ?? html); _Paste(data, options, hotkey, timeoutMS); } //problem: fails to paste in VMware player. Could add an option to not sync, but fails anyway because VMware gets clipboard with a big delay. //TODO2: add a tip here in doc or in cookbook: To create HTML you can use an online HTML editor, for example https://html-online.com/. /// /// Calls and handles exceptions. /// /// Returns false if failed. /// Max time to wait until the focused app gets clipboard data, in milliseconds. The function waits up to 10 times longer if the window is hung. /// Call . /// Call with text "Failed to paste text". /// public static bool tryPaste(string text, int timeout, bool warning = false, bool osd = false, string html = null, OKey options = null, [ParamString(PSFormat.Hotkey)] KHotkey hotkey = default) { try { paste(text, html, options, hotkey, Math.Max(timeout, 25)); return true; } catch (Exception e1) { if (warning) print.warning(e1); if (osd) osdText.showTransparentText("Failed to paste text"); return false; } } /// /// Calls and handles exceptions. /// /// Returns false if failed. /// Call . Default true. /// Call with text "Failed to paste text". Default true. /// [EditorBrowsable(EditorBrowsableState.Never)] //obsolete. Not optimal parameters. public static bool tryPaste(string text, string html = null, OKey options = null, bool warning = true, bool osd = true, [ParamString(PSFormat.Hotkey)] KHotkey hotkey = default, int timeoutMS = 0) { try { paste(text, html, options, hotkey, timeoutMS); return true; } catch (Exception e1) { if (warning) print.warning(e1); if (osd) osdText.showTransparentText("Failed to paste text"); return false; } } /// /// Pastes data added to a variable into the focused app using the clipboard. /// /// /// Paste data of two formats: HTML and text. /// text").AddText("text")); /// ]]> /// /// public static void pasteData(clipboardData data, OKey options = null, [ParamString(PSFormat.Hotkey)] KHotkey hotkey = default, int timeoutMS = 0) { Not_.Null(data); _Paste(data, options, hotkey, timeoutMS); } //rejected. Should use some UI-created/saved data containing all three formats. //public static void pasteRichText(string text, string rtf, string html = null, OKey options = null) //{ // var a = new List<(int, object)>(); // if(!text.NE()) a.Add((0, text)); // if(!rtf.NE()) a.Add((Lib.RtfFormat, rtf)); // if(!html.NE()) a.Add((Lib.HtmlFormat, html)); // if(a.Count == 0) return; // _Paste(a, options); //} static void _Paste(object data, OKey options, KHotkey hotkey, int timeoutMS) { var wFocus = keys.Internal_.GetWndFocusedOrActive(requireFocus: true); var optk = options ?? opt.key; using (var bi = new inputBlocker { ResendBlockedKeys = true }) { if (!optk.NoBlockInput) bi.Start(BIEvents.Keys); keys.Internal_.ReleaseModAndDisableModMenu(); optk = optk.GetHookOptionsOrThis_(wFocus); Paste_(data, optk, wFocus, hotkey, timeoutMS); } } /// /// Used by and . /// The caller should block user input (if need), release modifier keys, get optk/wFocus, sleep finally (if need). /// /// string or . internal static void Paste_(object data, OKey optk, wnd wFocus, KHotkey hotkey = default, int timeoutMS = 0) { bool isConsole = wFocus.IsConsole; List andKeys = null; if (optk.PasteWorkaround && data is string s && !isConsole) { var s2 = s.TrimEnd("\r\n\t "); if (s2 != s) { andKeys = new List(); for (int i = s2.Length; i < s.Length; i++) { char ch = s[i]; if (ch == '\n') { if (i > 0 && s[i - 1] == '\r') continue; ch = '\r'; } andKeys.Add((KKey)ch); } if (s2.Length == 0) { keys.Internal_.SendCopyPaste.AndSendKeys(andKeys, optk); return; } data = s2; } } bool sync = true; //CONSIDER: option to turn off, depending on window. _ClipboardListener listener = null; _DisableClipboardHistory disableCH = default; var oc = new OpenClipboard_(true); bool restore = optk.RestoreClipboard; _SaveRestore save = default; try { disableCH.Disable(); //fast if (restore) save.Save(); EmptyClipboard_(); _SetClipboardData_ClipboardViewerIgnore(); _SetClipboard(data, renderLater: sync); oc.Close(false); //close clipboard; don't destroy our clipboard owner window if (sync) listener = new _ClipboardListener(true, data, oc.WndClipOwner, wFocus); //info: // oc ctor creates a temporary message-only clipboard owner window. Its wndproc initially is DefWindowProc. // listener ctor subclasses it. Its wndproc receives WM_RENDERFORMAT which sets clipboard data etc. //PasteSync_?.Start(new(wFocus, optk, data)); var ctrlV = new keys.Internal_.SendCopyPaste(); try { if (isConsole) { wFocus.Post(Api.WM_SYSCOMMAND, 65521); //system menu &Edit > &Paste; tested on all OS; Windows 10 supports Ctrl+V, but it can be disabled. } else { if (hotkey.Key == 0) hotkey = new(KMod.Ctrl, KKey.V); ctrlV.Press(hotkey, optk, wFocus, andKeys: andKeys); } //wait until the app gets clipboard data if (sync) { listener.Wait(ref ctrlV, timeoutMS); if (listener.FailedToSetData != null) throw new AuException(listener.FailedToSetData.Message); if (listener.IsBadWindow) sync = false; } keys.Internal_.Sleep(optk.KeySpeedClipboard); //rejected: subtract the time already waited in listener.Wait. //never mind: if too long, in some apps may autorepeat, eg BlueStacks after 500 ms. Rare. } finally { ctrlV.Release(); } wFocus.SendTimeout(1000, out _, 0, flags: 0); //can help with some apps, but many apps eg browsers are async int sleep = optk.PasteSleep; if (!sync && sleep < 500) sleep = (sleep + 500) / 2; keys.Internal_.Sleep(sleep); //PasteSync_?.End(); } finally { if (restore && save.IsSaved) { if (oc.Reopen(true)) save.Restore(); } oc.Dispose(); disableCH.Restore(); } GC.KeepAlive(listener); } //internal static IPasteSync_ PasteSync_ { get; set; } //internal interface IPasteSync_ { // void Start(PasteSyncArgs_ p); // void End(); //} //internal record class PasteSyncArgs_(wnd wFocus, OKey optk, object data); /// /// Waits until the target app gets (when pasting) or sets (when copying) clipboard text. /// For it subclasses our clipboard owner window and uses clipboard messages. Does not unsubclass. /// class _ClipboardListener : WaitVariable_ { bool _paste; //true if used for paste, false if for copy object _data; //string or Data. null if !_paste. WNDPROC _wndProc; //wnd _wPrevClipViewer; wnd _wFocus; /// /// The clipboard message has been received. Probably the target window responded to the Ctrl+C or Ctrl+V. /// When pasting, it is unreliable because of clipboard viewers/managers/etc. The caller also must check IsBadWindow. /// public bool Success => waitVar; /// /// When pasting, true if probably not the target process retrieved clipboard data. Probably a clipboard viewer/manager/etc. /// Not used when copying. /// public bool IsBadWindow; /// /// Exception thrown/caught when failed to set clipboard data. /// public Exception FailedToSetData; /// /// Subclasses clipOwner. /// /// true if used for paste, false if for copy. /// If used for paste, can be string containing Unicode text or int/string dictionary containing clipboard format/data. /// Our clipboard owner window. /// The target control or window. public _ClipboardListener(bool paste, object data, wnd clipOwner, wnd wFocus) { _paste = paste; _data = data; _wndProc = _WndProc; _wFocus = wFocus; WndUtil.SubclassUnsafe_(clipOwner, _wndProc); //rejected: use SetClipboardViewer to block clipboard managers/viewers/etc. This was used in QM2. // Nowadays most such programs don't use SetClipboardViewer. They use AddClipboardFormatListener+WM_CLIPBOARDUPDATE. // known apps that have clipboard viewer installed with SetClipboardViewer: // OpenOffice, LibreOffice: tested Writer, Calc. // VLC: after first Paste. //_wPrevClipViewer = Api.SetClipboardViewer(clipOwner); //print.it(_wPrevClipViewer); } /// /// Waits until the target app gets (when pasting) or sets (when copying) clipboard text. /// Throws on timeout (default 3 s normally, 28 s if the target window is hung). /// /// The variable that was used to send Ctrl+V or Ctrl+C. This function may call Release to avoid too long Ctrl down. public void Wait(ref keys.Internal_.SendCopyPaste ctrlKey, int timeoutMS = 0) { //print.it(Success); //on Paste often already true, because SendInput dispatches sent messages int n = 6, t = 500; //max 3 s (6*500 ms). If hung, max 28 s. if (timeoutMS > 0) { n = (timeoutMS + 500 - 1) / 500; t = timeoutMS / n; } while (!Success) { wait.Wait_(t, WHFlags.DoEvents, stopVar: this); if (Success) break; if (--n == 0) throw new AuException(_paste ? "*paste" : "*copy"); ctrlKey.Release(); //is hung? _wFocus.SendTimeout(t * 10, out _, 0, flags: 0); } } nint _WndProc(wnd w, int message, nint wParam, nint lParam) { //WndUtil.PrintMsg(w, message, wParam, lParam); switch (message) { //case Api.WM_DESTROY: // Api.ChangeClipboardChain(w, _wPrevClipViewer); // break; case Api.WM_RENDERFORMAT: if (_paste && !Success) { IsBadWindow = !_IsTargetWindow(); //note: need to set clipboard data even if bad window. // Else the clipboard program may retry in loop. Eg Ditto. Then often pasting fails. // If IsBadWindow, we'll then sleep briefly. // Good clipboard programs get clipboard data with a delay. Therefore usually they don't interfere, unless the target app is very slow. // Eg Windows Clipboard History 200 ms. Eg Ditto default is 100 ms and can be changed. // Also, after setting clipboard data we cannot wait for good window, because we'll not receive second WM_RENDERFORMAT. try { _SetClipboard(_data, false); } catch (Exception ex) { FailedToSetData = ex; } //cannot throw in wndproc, will throw later waitVar = true; } return 0; case Api.WM_CLIPBOARDUPDATE: //posted, not sent. Once, not for each format. Added in WinVista. QM2 used SetClipboardViewer/WM_DRAWCLIPBOARD. if (!_paste) waitVar = true; return 0; } return Api.DefWindowProc(w, message, wParam, lParam); //Returns false if probably not the target app reads from the clipboard. Probably a clipboard viewer/manager/etc. bool _IsTargetWindow() { wnd wOC = Api.GetOpenClipboardWindow(); //int color = 0; if(wOC != _wFocus) color = wOC.ProcessId == _wFocus.ProcessId ? 0xFF0000 : 0xFF; //print.it($"<>{wOC}"); if (wOC == _wFocus) return true; if (wOC.Is0) return true; //tested: none of tested apps calls OpenClipboard(0) if (wOC.ProcessId == _wFocus.ProcessId) return true; //often classnamed "CLIPBRDWNDCLASS". Some clipboard managers too, eg Ditto. if (osVersion.minWin10 && 0 != _wFocus.Window.IsUwpApp) { var prog = wOC.ProgramName; if (prog.Eqi("svchost.exe")) return true; //if (prog.Eqi("RuntimeBroker.exe")) return true; //used to be Store apps //tested: no problems on Win8.1 } //tested: WinUI3 (cn "WinUIDesktopWin32WindowClass"): wOC != _wFocus, but same process. //CONSIDER: option to return true for user-known windows, eg using a callback. Print warning that includes wOC info. Debug_.Print(wOC.ToString()); return false; //BlueStacks problems: // Uses an aggressive viewer. Always debugprints while it is running, even when other apps are active. // Sometimes pastes old text, usually after starting BlueStacks or after some time of not using it. // With or without clipboard restoring. // Then starts to work correctly always. Difficult to debug. // KeySpeedClipboard=100 usually helps, but sometimes even 300 does not help. } } } /// /// Opens and closes clipboard using API OpenClipboard and CloseClipboard. /// Constructor tries to open for 10 s, then throws . /// If the createOwner parameter is true, creates temporary message-only clipboard owner window. /// If the noOpenNow parameter is true, does not open, only creates owner if need. /// Dispose closes clipboard and destroys the owner window. /// internal struct OpenClipboard_ : IDisposable { bool _isOpen; wnd _w; public wnd WndClipOwner => _w; public OpenClipboard_(bool createOwner, bool noOpenNow = false) { _isOpen = false; _w = default; if (createOwner) { _w = WndUtil.CreateWindowDWP_(messageOnly: true); //MSDN says, SetClipboardData fails if OpenClipboard called with 0 hwnd. It doesn't, but better use hwnd. } if (!noOpenNow) Reopen(); } /// /// Opens again. /// Must be closed. /// Owner window should be not destroyed; does not create again. /// /// If fails, return false, no exception. Also then waits 1 s instead of 10 s. /// Failed to open. public bool Reopen(bool noThrow = false) { Debug.Assert(!_isOpen); var loop = new WaitLoop(new(noThrow ? -1 : -10) { Period = 1 }); while (!Api.OpenClipboard(_w)) { int ec = lastError.code; if (!loop.Sleep()) { Dispose(); if (noThrow) return false; throw new AuException(ec, "*open clipboard"); } } _isOpen = true; return true; } public void Close(bool destroyOwnerWindow) { if (_isOpen) { Api.CloseClipboard(); _isOpen = false; } if (destroyOwnerWindow && !_w.Is0) { Api.DestroyWindow(_w); _w = default; } } public void Dispose() => Close(true); } /// /// Saves and restores clipboard data. /// Clipboard must be open. Don't need to call EmptyClipboard before Restore. /// struct _SaveRestore { Dictionary _data; public void Save(bool debug = false) { var p1 = new perf.Instance(); //will need if debug==true. Don't delete the perf statements, they are used by a public function. bool allFormats = OKey.RestoreClipboardAllFormats || debug; string[] exceptFormats = OKey.RestoreClipboardExceptFormats; for (int format = 0; 0 != (format = Api.EnumClipboardFormats(format));) { bool skip = false; string name = null; if (!allFormats) { skip = format != Api.CF_UNICODETEXT; } else { //standard, private if (format < Api.CF_MAX) { //standard switch (format) { case Api.CF_OEMTEXT: //synthesized from other text formats case Api.CF_BITMAP: //synthesized from DIB formats case Api.CF_PALETTE: //rare, never mind skip = true; break; case Api.CF_METAFILEPICT: case Api.CF_ENHMETAFILE: skip = true; //never mind, maybe in the future break; } } else if (format < 0xC000) { //CF_OWNERDISPLAY, DSP, GDI, private skip = true; //never mind. Not auto-freed, etc. Rare. } //else registered if (!skip && exceptFormats != null && exceptFormats.Length != 0) { name = ClipFormats.GetName(format); foreach (string s in exceptFormats) if (s.Eqi(name)) { skip = true; break; } } } if (debug) { name ??= ClipFormats.GetName(format); if (skip) print.it($"{name,-62} restore=False"); else p1.First(); //note: we don't call GetClipboardData for formats in exceptFormats, because the conditions must be like when really saving. Time of GetClipboardData(format2) may depend on whether called GetClipboardData(format1). } if (skip) continue; var data = Api.GetClipboardData(format); int size = (data == default) ? 0 : (int)Api.GlobalSize(data); if (size == 0 || size > 10 * 1024 * 1024) skip = true; //If data == default, probably the target app did SetClipboardData(NULL) but did not render data on WM_RENDERFORMAT. // If we try to save/restore, we'll receive WM_RENDERFORMAT too. It can be dangerous. if (debug) { p1.Next(); print.it($"{name,-32} time={p1.TimeTotal,-8} size={size,-8} restore={!skip}"); continue; } if (skip) continue; var b = Api.GlobalLock(data); Debug.Assert(b != default); if (b == default) continue; try { _data ??= new Dictionary(); var a = new byte[size]; Marshal.Copy(b, a, 0, size); _data.Add(format, a); } finally { Api.GlobalUnlock(data); } } } public void Restore() { if (_data == null) return; EmptyClipboard_(); foreach (var v in _data) { var a = v.Value; var h = Api.GlobalAlloc(Api.GMEM_MOVEABLE, a.Length); var b = Api.GlobalLock(h); if (b != default) { try { Marshal.Copy(a, 0, b, a.Length); } finally { Api.GlobalUnlock(h); } if (default == Api.SetClipboardData(v.Key, h)) b = default; } Debug.Assert(b != default); if (b == default) Api.GlobalFree(h); } } public bool IsSaved => _data != null; } /// /// Temporarily disables Windows 10 Clipboard History. /// Note: before disabling, we must open clipboard, else Clipboard History could be suspended while it has clipboard open. /// struct _DisableClipboardHistory { //Pasting is unreliable with Windows 10 Clipboard History (CH). //Sometimes does not paste because OpenClipboard fails in the target app, because then CH has it open. //Then also _IsTargetWindow debugprints. Often just debugprints and waits briefly, but pasting works. //CH is enabled by default. Can be disabled in Settings > System > Clipboard. //If enabled, CH opens clipboard and gets text after 200 ms, and then repeats every several ms, total ~15 times and 50 ms. // When the target app fails to OpenClipboard, Paste_ waits briefly and the script continues. We receive WM_RENDERFORMAT because CH gets text. //If disabled, CH still opens clipboard after 200 ms, total 1-3 times. // When the target app fails to OpenClipboard, Paste_ waits and fails, because does not receive WM_RENDERFORMAT. It seems CH does not get text. //Possible workarounds, maybe untested or unreliable or too crazy: // Hook posted messages (in C++ dll) and block WM_CLIPBOARDUPDATE. // Now using. // Simple and fast if using only 64-bit hook. // Less reliable because the message is posted async and can arrive after we remove hook. Never noticed, even with Task.Delay(1). // Does not work if our process is 32-bit. Also does not block viewers of different bitness processes. Never mind, it's rare and not so important. // Blocks CH when this process isn't admin too. // Blocks most other 64-bit clipboard viewers too. // Temporarily SuspendThread. Tested, simple, fast, reliable. // Find all "CLIPBRDWNDCLASS" message-only windows and suspend their threads. // The service process is in user session and not admin. // But then cannot copy/paste in winstore apps, eg Calculator and Stickynotes. // Temporarily stop service "Clipboard User Service_xxxxxxx". Tested. Disables CH completely. // Would be simple and fast, but need to find service name, it is with random suffix. // But need to run as admin. // When pasting, OS autotarts it again after 400-500 ms. // Pausing fails, but can stop/start. // OS does not allow to set startup type "Disabled". And auto-starts when eg the Settings page opened. // Inject a dll into the target process and hook OpenClipboard, let it wait until succeeds. Too crazy. // Send WM_CLOSE to the CH clipboard window (wOC). Tested, works, but too crazy. // Temporarily RemoveClipboardFormatListener. Does not work. //Plus there are other OS parts that use clipboard viewers. // Eg the Settings app in the Clipboard page opens/gets text after 500 ms, usually 2 times. // Note: these workarounds may not help while the Clipboard page is open. Then eg pasting often fails because the target app cannot open clipboard. //Tested with Ditto too. // Without workaround sometimes fails because the target app cannot open clipboard. // With this workaround (hook) never fails. #if true IntPtr _hh; //static int s_nhooks; //test how many hooks when eg pasting in loop with small sleep public void Disable() { //if (!osVersion.minWin10_1809 || osVersion.Is32BitProcessAnd64BitOS) return; //no, let's block clipboard viewers on all OS if (osVersion.is32BitProcessAnd64BitOS) return; //if (keys.isScrollLock) return; _hh = Cpp.Cpp_Clipboard(default); Debug.Assert(_hh != default); //print.it(++s_nhooks); //max 8 when all delays removed in script. Max 4-5 with default options. Max 3-4 with 10.ms() in loop. } public void Restore() { if (_hh == default) return; var hh = _hh; _hh = default; //remove hook later, when all posted WM_CLIPBOARDUPDATE probably are received. // In my quick tests it always worked reliably, even with delay 1 ms. But I did not test with many clipboard viewers and in stress conditions. Task.Delay(100).ContinueWith(_ => { Cpp.Cpp_Clipboard(hh); /*s_nhooks--;*/ }); //info: if this process ends sooner, OS removes the hook } #else List _a; public void Disable() { if (!osVersion.minWin10_1809) return; for (wnd w = default; ;) { w = wnd.findFast(null, "CLIPBRDWNDCLASS", true, w); if (w.Is0) break; int tid = w.GetThreadProcessId(out int pid); if (tid == 0) continue; if (!process.getName(pid, noSlowAPI: true).Eqi("svchost.exe")) continue; var ht = Api.OpenThread(Api.THREAD_SUSPEND_RESUME, false, tid); if (ht.Is0) continue; if (Api.SuspendThread(ht) < 0) { ht.Dispose(); continue; } _a ??= new List(); _a.Add(ht); } Debug_.PrintIf(_a == null, "no suspended threads"); } public void Restore() { if (_a == null) return; foreach (var ht in _a) { Api.ResumeThread(ht); ht.Dispose(); } } #endif } internal static void PrintClipboard_() { print.it("---- Clipboard ----"); using var oc = new OpenClipboard_(true); Api.GetClipboardData(0); //JIT var save = new _SaveRestore(); save.Save(true); } } ================================================ FILE: Au/Input/clipboardData.cs ================================================ //#define SUPPORT_RAW_HANDLE using System.Drawing; using System.Drawing.Imaging; namespace Au { /// /// Sets or gets clipboard data in multiple formats. /// /// /// The AddX functions add data to the variable (not to the clipboard). Then copies the added data to the clipboard. Also you can use the variable with . /// The static GetX functions get data directly from the clipboard. /// /// /// Get bitmap image from clipboard. /// /// Set clipboard data in two formats: text and image. /// /// Paste data of two formats: HTML and text. /// text").AddText("text")); /// ]]> /// Copy data in two formats: HTML and text. /// { html = clipboardData.getHtml(); text = clipboardData.getText(); }); /// print.it(html); print.it(text); /// ]]> /// public class clipboardData { struct _Data { public object data; public int format; } List<_Data> _a = new(); #region add static void _CheckFormat(int format, bool minimalCheckFormat = false) { bool badFormat = false; if (format <= 0 || format > 0xffff) badFormat = true; else if (format < 0xC000 && !minimalCheckFormat) { if (format >= Api.CF_MAX) badFormat = true; //rare. Most are either not GlobalAlloc'ed or not auto-freed. else badFormat = format == Api.CF_BITMAP || format == Api.CF_PALETTE || format == Api.CF_METAFILEPICT || format == Api.CF_ENHMETAFILE; //not GlobalAlloc'ed } if (badFormat) throw new ArgumentException("Invalid format id."); } clipboardData _Add(object data, int format, bool minimalCheckFormat = false) { Not_.Null(data); _CheckFormat(format, minimalCheckFormat); _a.Add(new _Data() { data = data, format = format }); return this; } /// /// Adds text. /// /// this. /// Text. /// /// Clipboard format id. Default: (CF_UNICODETEXT). /// Text encoding depends on format; default UTF-16. See . /// /// /// Invalid format. /// Exceptions of , which is called if encoding is not UTF-16. public clipboardData AddText(string text, int format = ClipFormats.Text) { Encoding enc = ClipFormats.GetTextEncoding_(format, out _); if (enc == null) return _Add(text, format == 0 ? Api.CF_UNICODETEXT : format); return _Add(enc.GetBytes(text).InsertAt(-1), format); } /// /// Adds data of any format as byte[]. /// /// this. /// byte[] containing data. /// Clipboard format id. See . /// /// Invalid format. Supported are all registered formats and standard formats <CF_MAX except GDI handles. public clipboardData AddBinary(byte[] data, int format) { return _Add(data, format); } //rejected: rarely used, difficult to use, creates problems. If somebody needs it, can use API. #if SUPPORT_RAW_HANDLE /// /// Adds data of any format as raw clipboard object handle. /// /// this. /// Any handle supported by API SetClipboardData. The type depends on format. For most formats, after setting clipboard data the handle is owned and freed by Windows. /// Clipboard format id. See . /// /// Invalid format. /// /// The same handle cannot be added to the clipboard twice. To avoid it, "set clipboard" functions remove handles from the variable. /// public Data AddHandle(IntPtr handle, int format) { return _Add(handle != default ? (object)handle : null, format, minimalCheckFormat: true); } #endif /// /// Adds image. /// Uses clipboard format and/or (CF_BITMAP). /// /// this. /// Image. Must be , else exception. /// /// Use PNG format (it supports transparency): ///
false - no, only CF_BITMAP. ///
true - yes, only PNG. ///
null (default) - add PNG and CF_BITMAP. /// /// public clipboardData AddImage(Image image, bool? png = null) { var b = (Bitmap)image; if (png != false) { var ms = new MemoryStream(); b.Save(ms, ImageFormat.Png); _Add(ms.ToArray(), ClipFormats.Png, minimalCheckFormat: true); } if (png != true) { _Add(b, Api.CF_BITMAP, minimalCheckFormat: true); } return this; } /// /// Adds HTML text. Uses clipboard format ("HTML Format"). /// /// this. /// Full HTML or HTML fragment. If full HTML, a fragment in it can be optionally specified. See examples. /// /// /// italy"); /// d.AddHtml("italy"); /// d.AddHtml("italy"); /// ]]> /// public clipboardData AddHtml(string html) { return AddBinary(CreateHtmlFormatData_(html), ClipFormats.Html); //note: don't support UTF-16 string of HTML format (starts with "Version:"). UTF8 conversion problems. } /// /// Adds list of files to copy/paste. Uses clipboard format (CF_HDROP). /// /// this. /// One or more file paths. /// public clipboardData AddFiles(params string[] files) { Not_.Null(files); var b = new StringBuilder("\x14\0\0\0\0\0\0\0\x1\0"); //struct DROPFILES foreach (var s in files) { b.Append(s); b.Append('\0'); } return _Add(b.ToString(), Api.CF_HDROP, false); } /// /// Copies the added data of all formats to the clipboard. /// /// Failed to open clipboard (after 10 s of wait/retry) or set clipboard data. /// Failed to allocate memory for clipboard data. /// /// Calls API OpenClipboard, EmptyClipboard, SetClipboardData and CloseClipboard. /// public void SetClipboard() { using (new clipboard.OpenClipboard_(true)) { clipboard.EmptyClipboard_(); SetOpenClipboard(); } } /// /// Copies the added data of all formats to the clipboard which is open/owned by this thread. /// /// Call API SetClipboardData: SetClipboardData(format, default). When/if some app will try to get clipboard data, the first time your clipboard owner window will receive WM_RENDERFORMAT message and should call SetOpenClipboard(false);. /// Copy data only of this format. If 0 (default), of all formats. /// Failed to allocate memory for clipboard data. /// Failed to set clipboard data. /// /// This function is similar to . It calls API SetClipboardData and does not call OpenClipboard, EmptyClipboard, CloseClipboard. The clipboard must be open and owned by a window of this thread. /// public void SetOpenClipboard(bool renderLater = false, int format = 0) { for (int i = 0; i < _a.Count; i++) { var v = _a[i]; if (format != 0 && v.format != format) continue; if (renderLater) { lastError.clear(); Api.SetClipboardData(v.format, default); int ec = lastError.code; if (ec != 0) throw new AuException(ec, "*set clipboard data"); } else _SetClipboard(v.format, v.data); } #if SUPPORT_RAW_HANDLE //remove caller-added handles, to avoid using the same handle twice if(renderLater) return; for(int i = _a.Count; --i >= 0;) { var v = _a[i]; if(format != 0 && v.format != format) continue; if(v.data is IntPtr) _a.RemoveAt(i); } #endif } static unsafe void _SetClipboard(int format, object data) { IntPtr h = default; switch (data) { case string s: fixed (char* p = s) h = _CopyToHmem(p, (s.Length + 1) * 2); break; case byte[] b: fixed (byte* p = b) h = _CopyToHmem(p, b.Length); break; #if SUPPORT_RAW_HANDLE case IntPtr ip: h = ip; break; #endif case Bitmap bmp: h = bmp.GetHbitmap(); var h2 = Api.CopyImage(h, 0, 0, 0, Api.LR_COPYDELETEORG); //DIB to compatible bitmap if (h2 == default) goto ge; h = h2; break; } Debug.Assert(h != default); if (default != Api.SetClipboardData(format, h)) return; ge: int ec = lastError.code; if (data is Bitmap) Api.DeleteObject(h); else Api.GlobalFree(h); throw new AuException(ec, "*set clipboard data"); } static unsafe IntPtr _CopyToHmem(void* p, int size) { var h = Api.GlobalAlloc(Api.GMEM_MOVEABLE, size); if (h == default) goto ge; var v = (byte*)Api.GlobalLock(h); if (v == null) { Api.GlobalFree(h); goto ge; } try { MemoryUtil.Copy(p, v, size); } finally { Api.GlobalUnlock(h); } return h; ge: throw new OutOfMemoryException(); } /// /// Copies Unicode text to the clipboard without open/empty/close. /// internal static void SetText_(string text) { Debug.Assert(text != null); _SetClipboard(Api.CF_UNICODETEXT, text); } /// /// Converts HTML string to byte[] containing data in clipboard format "HTML Format". /// /// Full HTML or HTML fragment. If full HTML, a fragment in it can be optionally specified. See examples. /// /// /// HTML examples. /// italy" /// "italy" /// "italy" /// ]]> /// internal static unsafe byte[] CreateHtmlFormatData_(string html) { Not_.Null(html); var b = new StringBuilder(c_headerTemplate); //find "..." and "..." in it int isb = -1, ieb = -1, isf = -1, ief = -1; //start/end of inner body and fragment if (html.RxMatch(@"", 0, out RXGroup body) && (ieb = html.Find("", body.End)) >= 0) { isb = body.End; isf = html.Find(c_startFragment, isb..ieb, true); if (isf >= 0) { isf += c_startFragment.Length; ief = html.Find(c_endFragment, isf..ieb, true); } } //print.it($"{isb} {ieb} {isf} {ief}"); if (ieb < 0) { //no "..." b.Append("").Append(c_startFragment).Append(html).Append(c_endFragment).Append(""); isf = 12 + c_startFragment.Length; ief = isf + Encoding.UTF8.GetByteCount(html); } else { if (ief < 0) { //"........." b.Append(html, 0, isb).Append(c_startFragment).Append(html, isb, ieb - isb) .Append(c_endFragment).Append(html, ieb, html.Length - ieb); isf = isb + c_startFragment.Length; ief = ieb + c_startFragment.Length; } else { //"..............." b.Append(html); isb = isf; ieb = ief; //reuse these vars to calc UTF8 lengths } //correct isf/ief if html part lengths are different in UTF8 if (!html.IsAscii()) { fixed (char* p = html) { int lenDiff1 = Encoding.UTF8.GetByteCount(p, isb) - isb; int lenDiff2 = Encoding.UTF8.GetByteCount(p + isb, ieb - isb) - (ieb - isb); isf += lenDiff1; ief += lenDiff1 + lenDiff2; } } } //print.it($"{isf} {ief}"); isf += c_headerTemplate.Length; ief += c_headerTemplate.Length; b.Append('\0'); var a = Encoding.UTF8.GetBytes(b.ToString()); _SetNum(a.Length - 1, 53); _SetNum(isf, 79); _SetNum(ief, 103); //print.it(Encoding.UTF8.GetString(a)); return a; void _SetNum(int num, int i) { for (; num != 0; num /= 10) a[--i] = (byte)('0' + num % 10); } } const string c_startFragment = ""; const string c_endFragment = ""; const string c_headerTemplate = @"Version:0.9 StartHTML:0000000105 EndHTML:0000000000 StartFragment:0000000000 EndFragment:0000000000 "; #endregion #region get struct _GlobalLock : IDisposable { IntPtr _hmem; public _GlobalLock(IntPtr hmem, out IntPtr mem, out int size) { mem = Api.GlobalLock(hmem); if (mem == default) { _hmem = default; size = 0; return; } size = (int)Api.GlobalSize(_hmem = hmem); } public void Dispose() { Api.GlobalUnlock(_hmem); } } /// /// Gets clipboard text without open/close. /// If format is 0, tries CF_UNICODETEXT and CF_HDROP. /// internal static unsafe string GetText_(int format) { IntPtr h = default; if (format == 0) { h = Api.GetClipboardData(Api.CF_UNICODETEXT); if (h == default) format = Api.CF_HDROP; } if (format == 0) format = Api.CF_UNICODETEXT; else { h = Api.GetClipboardData(format); if (h == default) return null; if (format == Api.CF_HDROP) return string.Join("\r\n", HdropToFiles_(h)); } using (new _GlobalLock(h, out var mem, out int len)) { if (mem == default) return null; var s = (char*)mem; var b = (byte*)s; Encoding enc = ClipFormats.GetTextEncoding_(format, out bool unknown); if (unknown) { if ((len & 1) != 0 || Ptr_.Length(b, len) > len - 2) enc = Encoding.Default; //autodetect //never mind: it is UTF-8, not ANSI. Rarely used, especially with non-ASCII text. } if (enc == null) { len /= 2; while (len > 0 && s[len - 1] == '\0') len--; return new string(s, 0, len); } else { //most apps add single '\0' at the end. Some don't add. Some add many, eg Dreamweaver. Trim all. int charLen = enc.GetByteCount("\0"); switch (charLen) { case 1: while (len > 0 && b[len - 1] == '\0') len--; break; case 2: for (int k = len / 2; k > 0 && s[k - 1] == '\0'; k--) len -= 2; break; case 4: var ip = (int*)s; for (int k = len / 4; k > 0 && ip[k - 1] == '\0'; k--) len -= 4; break; } return enc.GetString(b, len); //note: don't parse HTML format here. Let caller use GetHtml or parse itself. } } } /// /// Gets text from the clipboard. /// /// null if there is no text. /// /// Clipboard format id. Default: (CF_UNICODETEXT). /// If 0, tries to get text () or file paths (; returns multiline text). /// Text encoding depends on format; default UTF-16. See . /// /// Failed to open clipboard (after 10 s of wait/retry). public static string getText(int format = ClipFormats.Text) { using (new clipboard.OpenClipboard_(false)) { return GetText_(format); } } /// /// Gets clipboard data of any format as byte[]. /// /// null if there is no data of this format. /// Invalid format. Supported are all registered formats and standard formats <CF_MAX except GDI handles. /// Failed to open clipboard (after 10 s of wait/retry). public static byte[] getBinary(int format) { _CheckFormat(format); using (new clipboard.OpenClipboard_(false)) { var h = Api.GetClipboardData(format); if (h == default) return null; using (new _GlobalLock(h, out var mem, out int len)) { if (mem == default) return null; var b = new byte[len]; Marshal.Copy(mem, b, 0, len); return b; } } } /// /// Gets clipboard data of any format without copying to array. Uses a callback function. /// /// Callback function that receives data. The clipboard is open until it returns. The data is read-only. /// The return value of the callback function. Returns default(T) if there is no data of this format. /// public static T getBinary(int format, Func get) { _CheckFormat(format); using (new clipboard.OpenClipboard_(false)) { return _GetBinary(format, get); } } static T _GetBinary(int format, Func get) { var h = Api.GetClipboardData(format); if (h != default) { using (new _GlobalLock(h, out var mem, out int len)) { if (mem != default) return get(mem, len); //.NET does not allow Func, T> } } return default; } #if SUPPORT_RAW_HANDLE public static IntPtr GetHandle(int format) { _CheckFormat(format, minimalCheckFormat: true); using(new OpenClipboard_(false)) { return Api.GetClipboardData(format); } } #endif /// /// Gets image from the clipboard. /// Uses clipboard format or (CF_BITMAP). /// /// null if there is no data of this format. /// /// Use PNG format (it supports transparency): ///
false - no, only CF_BITMAP. ///
true - yes, only PNG. ///
null (default) - yes, but get CF_BITMAP if there is no PNG. /// /// Failed to open clipboard (after 10 s of wait/retry). /// Exceptions of or . public static unsafe Bitmap getImage(bool? png = null) { using (new clipboard.OpenClipboard_(false)) { if (png != false && _GetBinary(ClipFormats.Png, static (mem, len) => { using var ms = new UnmanagedMemoryStream((byte*)mem, len); return Image.FromStream(ms); }) is Bitmap b1) return b1; if (png != true) { var h = Api.GetClipboardData(Api.CF_BITMAP); if (h != default) { using var b = Image.FromHbitmap(h, Api.GetClipboardData(Api.CF_PALETTE)); //bottom-up 32Rgb (GDI) return b?.Clone(new(default, b.Size), PixelFormat.Format32bppArgb) as Bitmap; //top-down 32Argb (GDI+) } } return null; } } /// /// Gets HTML text from the clipboard. Uses clipboard format ("HTML Format"). /// /// null if there is no data of this format or if failed to parse it. /// Failed to open clipboard (after 10 s of wait/retry). public static string getHtml() => getHtml(out _, out _, out _); /// Fragment start index in the returned string. /// Fragment length. /// Source URL, or null if unavailable. /// public static string getHtml(out int fragmentStart, out int fragmentLength, out string sourceURL) { return ParseHtmlFormatData_(getBinary(ClipFormats.Html), out fragmentStart, out fragmentLength, out sourceURL); } internal static string ParseHtmlFormatData_(byte[] b, out int fragmentStart, out int fragmentLength, out string sourceURL) { //print.it(s); fragmentStart = fragmentLength = 0; sourceURL = null; if (b == null) return null; string s = Encoding.UTF8.GetString(b); int ish = s.Find("StartHTML:", true); int ieh = s.Find("EndHTML:", true); int isf = s.Find("StartFragment:", true); int ief = s.Find("EndFragment:", true); if (ish < 0 || ieh < 0 || isf < 0 || ief < 0) return null; isf = s.ToInt(isf + 14); if (isf < 0) return null; ief = s.ToInt(ief + 12); if (ief < isf) return null; ish = s.ToInt(ish + 10); if (ish < 0) ish = isf; else if (ish > isf) return null; ieh = s.ToInt(ieh + 8); if (ieh < 0) ieh = ief; else if (ieh < ief) return null; if (s.Length != b.Length) { if (ieh > b.Length) return null; _CorrectOffset(ref isf); _CorrectOffset(ref ief); _CorrectOffset(ref ish); _CorrectOffset(ref ieh); } else if (ieh > s.Length) return null; //print.it(ish, ieh, isf, ief); int isu = s.Find("SourceURL:", true), ieu; if (isu >= 0 && (ieu = s.FindAny("\r\n", (isu += 10)..)) >= 0) sourceURL = s[isu..ieu]; fragmentStart = isf - ish; fragmentLength = ief - isf; return s[ish..ieh]; void _CorrectOffset(ref int i) { i = Encoding.UTF8.GetCharCount(b, 0, i); } } /// /// Gets file paths from the clipboard. Uses clipboard format (CF_HDROP). /// /// null if there is no data of this format. /// Failed to open clipboard (after 10 s of wait/retry). public static string[] getFiles() { using (new clipboard.OpenClipboard_(false)) { var h = Api.GetClipboardData(Api.CF_HDROP); if (h == default) return null; return HdropToFiles_(h); } } /// /// Gets file paths from HDROP. /// /// Array of zero or more non-null elements. internal static unsafe string[] HdropToFiles_(IntPtr hdrop) { int n = Api.DragQueryFile(hdrop, -1, null, 0); var a = new string[n]; var b = stackalloc char[500]; for (int i = 0; i < n; i++) { int len = Api.DragQueryFile(hdrop, i, b, 500); a[i] = new string(b, 0, len); } return a; } #endregion #region contains /// /// Returns true if the clipboard contains data of the specified format. /// /// Clipboard format id. See . /// Calls API IsClipboardFormatAvailable. public static bool contains(int format) { return Api.IsClipboardFormatAvailable(format); } /// /// Returns the first of the specified formats that is in the clipboard. /// Returns 0 if the clipboard is empty. Returns -1 if the clipboard contains data but not in any of the specified formats. /// /// Clipboard format ids. See . /// Calls API GetPriorityClipboardFormat. public static int contains(params int[] formats) { return Api.GetPriorityClipboardFormat(formats, formats.Length); } #endregion //CONSIDER: EnumFormats, OnClipboardChanged } } namespace Au.Types { /// /// Some clipboard format ids. /// These and other standard and registered format ids can be used with class functions. /// public static class ClipFormats { /// The text format. Standard, API constant CF_UNICODETEXT. The default format of add/get text functions. public const int Text = Api.CF_UNICODETEXT; /// The image format. Standard, API constant CF_BITMAP. Used by add/get image functions. public const int Image = Api.CF_BITMAP; /// The file-list format. Standard, API constant CF_HDROP. Used by add/get files functions. public const int Files = Api.CF_HDROP; /// The HTML format. Registered, name "HTML Format". Used by add/get HTML functions. public static int Html { get; } = Api.RegisterClipboardFormat("HTML Format"); /// The PNG format. Registered, name "PNG". Used by add/get image functions. public static int Png { get; } = Api.RegisterClipboardFormat("PNG"); /// Registered format "Shell IDList Array". internal static int ShellIDListArray_ { get; } = Api.RegisterClipboardFormat("Shell IDList Array"); /// Registered format "FileGroupDescriptorW". internal static int FileGroupDescriptorW_ { get; } = Api.RegisterClipboardFormat("FileGroupDescriptorW"); /// /// Registered format "Clipboard Viewer Ignore". /// /// /// Some clipboard viewer/manager programs don't try to get clipboard data if this format is present. For example Ditto, Clipdiary. /// The copy/paste functions of this library add this format to the clipboard to avoid displaying the temporary text/data in these programs, which also could make the paste function slower and less reliable. /// public static int ClipboardViewerIgnore { get; } = Api.RegisterClipboardFormat("Clipboard Viewer Ignore"); /// /// Registers a clipboard format and returns its id. If already registered, just returns id. /// /// Format name. /// Text encoding, if it's a text format. Used by , and functions that call them. For example . If null, text of unknown formats is considered Unicode UTF-16 (no encoding/decoding needed). /// Calls API RegisterClipboardFormat. public static int Register(string name, Encoding textEncoding = null) { var R = Api.RegisterClipboardFormat(name); if (textEncoding != null && R != 0 && R != Html) s_textEncoding[R] = textEncoding; return R; } static readonly ConcurrentDictionary s_textEncoding = new(); /// /// Gets text encoding for format. /// Returns null if UTF-16 or if the format is unknown and not in s_textEncoding. /// internal static Encoding GetTextEncoding_(int format, out bool unknown) { unknown = false; if (format is 0 or Api.CF_UNICODETEXT or Api.CF_HDROP) return null; if (format < Api.CF_MAX) return Encoding.Default; //never mind: it is UTF-8, not ANSI. Rarely used, especially with non-ASCII text. if (format == Html) return Encoding.UTF8; if (s_textEncoding.TryGetValue(format, out var enc)) return enc == Encoding.Unicode ? null : enc; unknown = true; return null; } /// /// Gets clipboard format name. /// /// A registered or standard clipboard format. If standard, returns string like "CF_BITMAP". /// Return null if format is unknown. If false, returns format as string. /// /// Calls API GetClipboardFormatName. Although undocumented, it also can get other strings from the same system atom table, for example registered Windows message names and window class names. /// [SkipLocalsInit] public static unsafe string GetName(int format, bool orNull = false) { //registered if (format >= 0xC000 && format <= 0xffff) { var b = stackalloc char[300]; int len = Api.GetClipboardFormatName(format, b, 300); if (len > 0) return new string(b, 0, len); } //standard var s = format switch { Api.CF_TEXT => "CF_TEXT", Api.CF_BITMAP => "CF_BITMAP", Api.CF_METAFILEPICT => "CF_METAFILEPICT", Api.CF_SYLK => "CF_SYLK", Api.CF_DIF => "CF_DIF", Api.CF_TIFF => "CF_TIFF", Api.CF_OEMTEXT => "CF_OEMTEXT", Api.CF_DIB => "CF_DIB", Api.CF_PALETTE => "CF_PALETTE", Api.CF_RIFF => "CF_RIFF", Api.CF_WAVE => "CF_WAVE", Api.CF_UNICODETEXT => "CF_UNICODETEXT", Api.CF_ENHMETAFILE => "CF_ENHMETAFILE", Api.CF_HDROP => "CF_HDROP", Api.CF_LOCALE => "CF_LOCALE", Api.CF_DIBV5 => "CF_DIBV5", _ => null }; return s ?? (orNull ? null : format.ToS()); } /// /// Gets formats currently in the clipboard. /// /// /// /// public static IEnumerable EnumClipboard() { using var oc = new clipboard.OpenClipboard_(true); for (int format = 0; 0 != (format = Api.EnumClipboardFormats(format));) { yield return format; } } } } ================================================ FILE: Au/Input/inputBlocker.cs ================================================ namespace Au { /// /// Blocks keyboard and/or mouse input events from reaching applications. /// /// /// Uses keyboard and/or mouse hooks. Does not use API BlockInput, it does not work on current Windows versions. /// Blocks hardware-generated events and software-generated events, except generated by functions of this library. /// Functions of this library that send keys or text use this class internally, to block user-pressed keys and resend them afterwards (see ). /// Does not block: /// - In windows of the same thread that started blocking. For example, if your script shows a message box, the user can click its buttons. /// - In windows of higher [](xref:uac) integrity level (IL) processes, unless this process has uiAccess IL. /// - In special desktops/screens, such as when you press Ctrl+Alt+Delete or launch an admin program that requires UAC elevation. See also . /// - Some Windows hotkeys, such as Ctrl+Alt+Delete and Win+L. /// - Keyboard hooks don't work in windows of this process if this process uses direct input or raw input API. /// /// To stop blocking, can be used using, like in the example. Or try with finally code that calls or . Also automatically stops when this thread ends. Users can stop with Ctrl+Alt+Delete. /// /// /// /// public sealed unsafe class inputBlocker : IDisposable { Handle_ _syncEvent, _stopEvent; Handle_ _threadHandle; keys _blockedKeys; long _startTime; BIEvents _block; int _threadId; bool _disposed; bool _discardBlockedKeys; //note: don't use API BlockInput because: // UAC. Fails if our process has Medium IL. // Too limited, eg cannot block only keys or only mouse. /// /// This constructor does nothing (does not call ). /// public inputBlocker() { } /// /// This constructor calls . /// /// what is 0. public inputBlocker(BIEvents what) { Start(what); } /// /// Starts blocking. /// /// what is 0. /// Already started. public void Start(BIEvents what) { if (_disposed) throw new ObjectDisposedException(nameof(inputBlocker)); if (_block != 0) throw new InvalidOperationException(); if (!what.HasAny(BIEvents.All)) throw new ArgumentException(); _block = what; _startTime = Environment.TickCount64; _syncEvent = Api.CreateEvent(false); _stopEvent = Api.CreateEvent(false); _threadHandle = Api.OpenThread(Api.SYNCHRONIZE, false, _threadId = Api.GetCurrentThreadId()); ThreadPool.QueueUserWorkItem(_this => (_this as inputBlocker)._ThreadProc(), this); //TODO3: what if thread pool is very busy? Eg if scripts use it incorrectly. Maybe better have own internal pool. Api.WaitForSingleObject(_syncEvent, Timeout.Infinite); GC.KeepAlive(this); } /// /// Calls . /// public void Dispose() { if (!_disposed) { _disposed = true; Stop(); GC.SuppressFinalize(this); } } /// ~inputBlocker() => _CloseHandles(); void _CloseHandles() { if (!_syncEvent.Is0) { _syncEvent.Dispose(); _stopEvent.Dispose(); _threadHandle.Dispose(); } } /// /// Stops blocking. /// Plays back blocked keys if need. See . /// Does nothing if currently is not blocking. /// /// Do not play back blocked key-down events recorded because of . public void Stop(bool discardBlockedKeys = false) { if (_block == 0) return; _block = 0; _discardBlockedKeys = discardBlockedKeys; Api.SetEvent(_stopEvent); Api.WaitForSingleObject(_syncEvent, Timeout.Infinite); _CloseHandles(); } const int c_maxResendTime = 10000; void _ThreadProc() { WindowsHook hk = null, hm = null; WinEventHook hwe = null; try { try { if (_block.Has(BIEvents.Keys)) hk = WindowsHook.Keyboard(_keyHookProc ??= _KeyHookProc); if (_block.HasAny(BIEvents.MouseClicks | BIEvents.MouseMoving)) hm = WindowsHook.Mouse(_mouseHookProc ??= _MouseHookProc); } catch (AuException e1) { Debug_.Print(e1); _block = 0; return; } //failed to hook //This prevents occassional inserting a foreign key after the first our-script-pressed key. //To reproduce, let our script send small series of chars in loop, and simultaneously a foreign script send other chars. wait.doEvents(); //print.it("started"); Api.SetEvent(_syncEvent); //the hook detects Ctrl+Alt+Del, Win+L, UAC consent, etc. SystemEvents.SessionSwitch only Win+L. try { hwe = new WinEventHook(EEvent.SYSTEM_DESKTOPSWITCH, 0, _winEventProc ??= _WinEventProc); } catch (AuException e1) { Debug_.Print(e1); } //failed to hook wait.Wait_(-1, WHFlags.DoEvents, new IntPtr[] { _stopEvent, _threadHandle }); if (_blockedKeys != null) { bool onlyUp = _discardBlockedKeys || Environment.TickCount64 - _startTime > c_maxResendTime; _blockedKeys.SendBlocked_(onlyUp); } //print.it("ended"); } finally { _blockedKeys = null; hk?.Dispose(); hm?.Dispose(); hwe?.Dispose(); Api.SetEvent(_syncEvent); } GC.KeepAlive(this); } Action _keyHookProc; Action _mouseHookProc; Action _winEventProc; void _KeyHookProc(HookData.Keyboard x) { if (_DontBlock(x.IsInjected, x.dwExtraInfo, x.vkCode)) { //print.it("ok", x.vkCode, !x.IsUp); return; } //print.it(message, x.vkCode); //if(x.vkCode == KKey.Delete && !x.IsUp) { // //Could detect Ctrl+Alt+Del here. But SetWinEventHook(SYSTEM_DESKTOPSWITCH) is better. //} if (ResendBlockedKeys && Environment.TickCount64 - _startTime < c_maxResendTime) { //If Shift is set to turn off CapsLock, on Shift the hook receives LShift down and CapsLock down/up with no 'injected' flag, even if the Shift was pressed by a script. //If we resend them, the hook catches the resent keys, and the resent LShift creates an infinite loop (actually for 10 s, see c_maxResendTime). //Workaround: don't resend these keys if isCapsLock and the system setting is active. //Also SendBlocked_ prevents such infinite loop in any case. bool no = false; if (x.vkCode == KKey.CapsLock || (x.vkCode == KKey.LShift && !x.IsUp)) { no = keys.isCapsLock && keys.IsCapsLockShiftOff_(); } if (!no) { _blockedKeys ??= new keys(null); //print.it("blocked", x.vkCode, !x.IsUp, x.IsInjected); _blockedKeys.AddRaw_(x.vkCode, (ushort)x.scanCode, x.SendInputFlags_); } } x.BlockEvent(); } void _MouseHookProc(HookData.Mouse x) { bool isMMove = x.Event == HookData.MouseEvent.Move; switch (_block & (BIEvents.MouseClicks | BIEvents.MouseMoving)) { case BIEvents.MouseClicks | BIEvents.MouseMoving: break; case BIEvents.MouseClicks: if (isMMove) return; break; case BIEvents.MouseMoving: if (!isMMove) return; break; } if (!_DontBlock(x.IsInjected, x.dwExtraInfo, 0, isMMove)) x.BlockEvent(); } bool _DontBlock(bool isInjected, nint extraInfo, KKey vk = 0, bool isMMove = false) { if (_pause) return true; if (isInjected) { //if(DontBlockInjected || (extraInfo != default && extraInfo == DontBlockInjectedExtraInfo)) return true; if (DontBlockInjected) return true; } wnd w; if (vk != 0) { //var a = DontBlockKeys; //if(a != null) foreach(var k in a) if(vk == k) return true; w = wnd.active; } else { w = isMMove ? wnd.active : wnd.fromMouse(); //note: don't use hook's pt, because of a bug in some OS versions. //note: for wheel it's better to use FromMouse. } if (w.ThreadId == _threadId) return true; return false; } void _WinEventProc(HookData.WinEvent x) { //the hook is called before and after Ctrl+Alt+Del screen. Only idEventThread different. // GetForegroundWindow returns 0. WTSGetActiveConsoleSessionId returns main session. //print.it("desktop switch"); //return; _startTime = 0; //don't resend Ctrl+Alt+Del and other blocked keys if (!ResumeAfterCtrlAltDelete) ThreadPool.QueueUserWorkItem(_this => (_this as inputBlocker).Stop(), this); } /// /// Continue blocking when returned from a special screen where blocking is disabled: Ctrl+Alt+Delete, [](xref:uac) consent, etc. /// public bool ResumeAfterCtrlAltDelete { get; set; } /// /// Record blocked keys, and play back when stopped blocking. /// /// /// Will not play back if: 1. The blocking time is > 10 seconds; then plays back only key-up events. 2. Detected Ctrl+Alt+Delete, [](xref:uac) consent or some other special screen. 3. Called . /// public bool ResendBlockedKeys { get; set; } /// /// Don't block software-generated key/mouse events. /// If false (default), only events generated by functions of this library are not blocked. /// public bool DontBlockInjected { get; set; } //rejected. Will be added later if need. Maybe a callback instead. ///// ///// Don't block software-generated key/mouse events if this extra info value was set when calling API SendInput. ///// ///// ///// Regardless of the property value, events generated by functions of this library are never blocked. ///// //public nint DontBlockInjectedExtraInfo { get; set; } ///// ///// Don't block these keys. ///// ///// ///// For modifier keys use the left/right key code: LCtrl, RCtrl, LShift, RShift, LAlt, RAlt, Win, RWin. ///// //public static KKey[] DontBlockKeys { get; set; } /// /// Gets or sets whether the blocking is paused. /// /// /// The set function is much faster than /. Does not remove hooks etc. Discards blocked keys. /// public bool Pause { get => _pause; set { _pause = value; _startTime = 0; //don't resend blocked keys } } bool _pause; } } namespace Au.Types { /// /// Used with class to specify what user input types to block (keys, mouse). /// [Flags] public enum BIEvents { /// /// Do not block. /// None, /// /// Block keys. Except if generated by functions of this library. /// Keys = 1, /// /// Block mouse clicks and wheel. Except if generated by functions of this library. /// MouseClicks = 2, /// /// Block mouse moving. Except if generated by functions of this library. /// MouseMoving = 4, /// /// Block keys, mouse clicks, wheel and mouse moving. Except if generated by functions of this library. /// This flag combines all other non-zero flags. /// All = 7, } } ================================================ FILE: Au/Input/keys.cs ================================================ //TODO3: on exception release modifiers. namespace Au; /// /// Keyboard functions. Send virtual keystrokes and text to the active window, get key state, wait for key. /// /// /// The main function is . Most documentation is there. See also . These functions use . Alternatively can be used keys variables, see . /// /// /// /// public partial class keys { /// Options to be copied to of this variable. Usually opt.key (current ambient options) or null (default options). /// /// /// public keys(OKey cloneOptions) { Options = new OKey(cloneOptions); } /// /// Options used by this variable. /// public OKey Options { get; } //KEYEVENTF_ flags for API SendInput. [Flags] enum _KFlags : byte { Extended = 1, Up = 2, Unicode = 4, Scancode = 8, }; //_KEvent type - key, text, sleep, etc. enum _KType : byte { KeyEvent, //send key down or up event, depending on _KFlags.Up. In _KEvent used vk and scan. KeyPair, //send key down and up events. In _KEvent used vk and scan. Char, //send character using keys. In _KEvent used ch. Text, //send text. In _KEvent used data, it is _data or _data element index. Callback, //call callback function. In _KEvent used data, it is _data or _data element index. Repeat, //repeat previous key. In _KEvent used repeat. Sleep, //sleep. In _KEvent used sleep. } [StructLayout(LayoutKind.Explicit)] struct _KEvent { [FieldOffset(0)] internal KKey vk; //byte [FieldOffset(1)] byte _flags; //_KFlags in 0x0F and _KType in 0xF0 [FieldOffset(2)] internal ushort scan; //scan code if IsKey [FieldOffset(2)] internal ushort data; //_data or _data index if IsText or IsCallback [FieldOffset(2)] internal ushort repeat; //repeat count if IsRepeat [FieldOffset(2)] internal ushort sleep; //milliseconds if IsSleep [FieldOffset(2)] internal ushort ch; //character if IsChar //Event type KeyEvent or KeyPair. internal _KEvent(bool pair, KKey vk, _KFlags siFlags, ushort scan = 0) { this.vk = vk; var f = (byte)siFlags; if (pair) f |= 16; _flags = f; this.scan = scan; } //Event of any type except KeyEvent and KeyPair. internal _KEvent(_KType type, ushort data) { Debug.Assert(type > _KType.KeyPair); _flags = (byte)((byte)type << 4); this.data = data; } internal _KType Type => (_KType)(_flags >> 4); internal bool IsPair => Type is _KType.KeyPair; internal bool IsKey => Type <= _KType.KeyPair; internal bool IsChar => Type == _KType.Char; internal bool IsKeyOrChar => Type <= _KType.Char; internal bool IsText => Type == _KType.Text; internal bool IsCallback => Type == _KType.Callback; internal bool IsRepeat => Type == _KType.Repeat; internal bool IsSleep => Type == _KType.Sleep; internal bool IsUp => 0 != (_flags & 2); internal _KFlags SIFlags => (_KFlags)(_flags & 15); internal void MakeDown() => _flags &= 9; internal void MakeUp() => _flags = (byte)((_flags & 9) | 2); #if DEBUG public override string ToString() { if (IsText) { Debug.Assert(SIFlags == 0); return $"text " + data; } if (IsCallback) { Debug.Assert(SIFlags == 0); return $"callback " + data; } if (IsSleep) { Debug.Assert(SIFlags == 0); return "sleep " + sleep; } if (IsRepeat) { Debug.Assert(SIFlags == 0); return "repeat " + repeat; } if (IsChar) { Debug.Assert(SIFlags == 0); return "char " + ch; } return $"{vk,-12} scan={scan,-4} flags={_flags}"; } #endif } //This struct is used to separate parsing-only fields from other fields. struct _KParsingState { public Stack<_KEvent> mod; //pushed on "+" or "+(". Then popped on key not preceded by +, and also in Send(). public bool paren; //we are between "+(" and ")" public bool plus; //we are between "+" and key or text } //This struct is used to separate sending-only fields from other fields. struct _KSendingState { public wnd wFocus; public OKey options; public void Clear() { wFocus = default; } } readonly List<_KEvent> _a = new(); //all key events and elements for each text/callback/repeat/sleep object _data; //text and callback parts. If there is 1 such part, it is string or Action; else it is List. _KParsingState _pstate; //parsing state _KSendingState _sstate; //sending state bool _sending; //while sending, don't allow to add or send bool? _antiCapsLock; /// /// Adds keystrokes to the internal collection. They will be sent by . /// /// This. /// /// [Key names and operators](xref:key_names), like with . Can be null or "". /// Example: "Tab Ctrl+V Alt+(E P) Left*3 Space a , 5 #5". /// If has prefix "!" or "%", calls ; use "!" for text, "%" for HTML. /// /// Error in keys_ string, for example an unknown key name. public keys AddKeys([ParamString(PSFormat.Keys)] string keys_) { _ThrowIfSending(); var k = keys_; if (k.NE()) return this; if (k[0] == '!') return AddText(k[1..]); if (k[0] == '%') return AddText(null, k[1..]); int i = 0, len = 0; foreach (var g in _SplitKeysString(k)) { //print.it($"<>{g.Value}"); //continue; i = g.Start; len = g.Length; char c = k[i]; _KEvent e; switch (c) { case '*': if (len == 1 || _a.Count == 0) goto ge; e = _a[^1]; char cLast = k[i + len - 1]; switch (cLast) { case 'n': //down case 'p': //up if (e.IsPair) { //make the last key down-only or up-only if (cLast == 'p') e.MakeUp(); else e.MakeDown(); _a[^1] = e; } else if (cLast == 'p' && _FindLastKey(out e)) { //allow eg Key("A*down*3*up") or Key("A*down", 500, "*up") e.MakeUp(); _a.Add(e); } else goto ge; break; default: //repeat if (!e.IsKeyOrChar) goto ge; AddRepeat(k.ToInt(i + 1)); break; } break; case '+': if (_pstate.paren || _a.Count == 0) goto ge; e = _a[^1]; if (!e.IsPair) goto ge; e.MakeDown(); _a[^1] = e; e.MakeUp(); _pstate.mod ??= new Stack<_KEvent>(); _pstate.mod.Push(e); if (len > 1) _pstate.paren = true; //"*(" else _pstate.plus = true; break; case ')': if (!_pstate.paren) goto ge; _pstate.paren = false; _AddModUp(); break; case '_' when len == 2: AddChar(k[i + 1]); break; case '^': if (_pstate.paren) goto ge; if (++i == g.End) break; if (g.End - i == 1 || _pstate.plus) { while (i < g.End) AddChar(k[i++]); } else { //avoid eg Shift up/down between AB _AddTextAndHow(k[i..], OKeyText.KeysOrChar, true); } break; //case '!': //rejected. Too many rules. Better slightly longer code than 2 ways to do the same. // AddText(k[++i..]); // break; default: //rejected: if non-ASCII, use AddChar. Why to add yet another rule for something rarely used. var vk = _KeynameToKey(k, i, len); if (vk == 0) goto ge; AddKey(vk); //print.it(vk); break; } } return this; ge: throw _ArgumentException_ErrorInKeysString(k, i, len); bool _FindLastKey(out _KEvent e) { for (int j = _a.Count; --j >= 0;) { var t = _a[j]; if (t.IsKey) { e = t; return true; } } e = default; return false; } } //Adds mod up events if need: if _parsing.mod is not empty and + is not active and ( is not active. void _AddModUp() { if (!_pstate.plus && !_pstate.paren && _pstate.mod != null) { while (_pstate.mod.Count != 0) _a.Add(_pstate.mod.Pop()); } } //Adds key or other event. Calls _ModUp(). Not used fo sleep and repeat. keys _AddKEvent(_KEvent e) { _AddModUp(); _pstate.plus = false; _a.Add(e); return this; } /// /// Adds single key, specified as , to the internal collection. It will be sent by . /// /// This. /// Virtual-key code, like KKey.Tab or (KKey)200. Valid values are 1-255. /// true - key down; false - key up; null (default) - key down-up. /// Invalid key (0). public keys AddKey(KKey key, bool? down = null) { _ThrowIfSending(); if (key == 0) throw new ArgumentException("Invalid value.", nameof(key)); _KFlags f = 0; if (down == false) f |= _KFlags.Up; if (KeyTypes_.IsExtended(key)) f |= _KFlags.Extended; return _AddKEvent(new _KEvent(down == null, key, f)); } /// /// Adds single key to the internal collection. Allows to specify scan code and whether it is an extended key. It will be sent by . /// /// This. /// Virtual-key code, like KKey.Tab or (KKey)200. Valid values are 1-255. Can be 0. /// Scan code of the physical key. Scan code values are 1-127, but this function allows 1-0xffff. Can be 0. /// true if the key is an extended key. /// true - key down; false - key up; null (default) - key down-up. /// Invalid scan code. public keys AddKey(KKey key, ushort scanCode, bool extendedKey, bool? down = null) { _ThrowIfSending(); _KFlags f = 0; if (key == 0) f = _KFlags.Scancode; else { //don't: if extendedKey false, set true if need. Don't do it because this func is 'raw'. } if (down == false) f |= _KFlags.Up; if (extendedKey) f |= _KFlags.Extended; return _AddKEvent(new _KEvent(down == null, key, f, scanCode)); } /// /// Adds single character to the internal collection. It will be sent like text with option . /// /// This. public keys AddChar(char c) { _ThrowIfSending(); return _AddKEvent(new _KEvent(_KType.Char, c)); } /// /// Adds key down or up event. /// /// /// /// SendInput flags. internal keys AddRaw_(KKey vk, ushort scan, byte siFlags) { _ThrowIfSending(); return _AddKEvent(new _KEvent(false, vk, (_KFlags)(siFlags & 0xf), scan)); } /// /// Sends key events added by AddRaw_ (called by ). /// Simply calls Api.SendInput. No options, no sleep, etc. No exceptions. /// If new events added while sending, sends them too, until there are no new events added. /// /// Send only "up" events. internal unsafe void SendBlocked_(bool onlyUp) { for (int ii = 0; ii < 5; ii++) { int n = 0; var a = new Api.INPUTK[_a.Count]; for (int i = 0; i < _a.Count; i++) { var k = _a[i]; if (onlyUp && !k.IsUp) continue; a[n++].Set(k.vk, k.scan, (uint)k.SIFlags); } _a.Clear(); if (n == 0) return; fixed (Api.INPUTK* p = a) Api.SendInput(p, n, dontThrow: true); //wait.doEvents(); //sometimes catches one more event, but not necessary if (_a.Count == 0) break; Debug_.PrintIf(ii == 4, "loop?"); //The hook proc is called while in SendInput. If we don't retry, new blocked keys are lost. // But don't retry forever, because in some cases OS injects keys and the hook receives them not marked as injected, eg on Shift if it is set to turn off CapsLock. } } /// /// Adds text or HTML. It will be sent by . /// /// This. /// /// To send text can use keys, characters or clipboard, depending on and text. If html not null, uses clipboard. /// /// public keys AddText(string text, string html = null) { _ThrowIfSending(); if (!html.NE()) { var data = new clipboardData().AddHtml(html).AddText(text ?? html); var ke = new _KEvent(_KType.Text, _SetData(data)); _AddKEvent(ke); } else if (!text.NE()) { var ke = new _KEvent(_KType.Text, _SetData(text)); _AddKEvent(ke); } return this; } /// /// Adds text with explicitly specified sending method (keys, characters or paste). /// /// This. /// Text. Can be null. /// Overrides . public keys AddText(string text, OKeyText how) { _ThrowIfSending(); _AddTextAndHow(text, how, false); return this; } void _AddTextAndHow(string text, OKeyText how, bool keysArg) { if (!text.NE()) { int flags = (int)how | 0x80; if (keysArg) flags |= 0x40; var ke = new _KEvent(_KType.Text, _SetData(text)) { vk = (KKey)flags }; _AddKEvent(ke); } } /// /// Adds clipboard data, for example several formats. It will be pasted by . /// /// This. /// Clipboard data. public keys AddClipboardData(clipboardData cd) { Not_.Null(cd); _ThrowIfSending(); _AddKEvent(new _KEvent(_KType.Text, _SetData(cd))); return this; } //Adds text (string) or clipboardData or callback (Action) to _data. ushort _SetData(object x) { int i; if (_data == null) { i = 0; _data = x; } else if (_data is List a) { i = a.Count; a.Add(x); } else { i = 1; _data = new List() { _data, x }; } return checked((ushort)i); } //Gets text (string) or clipboardData or callback (Action) from _data. object _GetData(ushort i) { if (_data is List a) return a[i]; return _data; } /// /// Adds a callback function. /// /// This. /// /// /// The callback function will be called by and can do anything except sending keys and copy/paste. /// public keys AddAction(Action a) { Not_.Null(a); _ThrowIfSending(); return _AddKEvent(new _KEvent(_KType.Callback, _SetData(a))); } /// /// Adds the repeat operator. Then will send the last added key or character count times. /// /// This. /// The repeat count. /// count >10000 or <0. /// The last added item is not a key or single character. public keys AddRepeat(int count) { _ThrowIfSending(); if ((uint)count > 10000) throw new ArgumentOutOfRangeException(nameof(count), "Max repeat count is 10000."); int i = _a.Count; if (i == 0 || !_a[i - 1].IsKeyOrChar) throw new ArgumentException("No key to repeat."); _a.Add(new _KEvent(_KType.Repeat, (ushort)count)); return this; } /// /// Adds a short pause. Then will sleep (wait). /// /// This. /// Time to sleep, milliseconds. /// timeMS >10000 (1 minute) or <0. public keys AddSleep(int timeMS) { _ThrowIfSending(); if ((uint)timeMS > 10000) throw new ArgumentOutOfRangeException(nameof(timeMS), "Max sleep time is 10000."); _a.Add(new _KEvent(_KType.Sleep, (ushort)timeMS)); return this; } /// /// Adds keystrokes, text, sleep and other events to the internal collection. They will be sent/executed by . /// /// This. /// public keys Add([ParamString(PSFormat.Keys)] params KKeysEtc[] keysEtc) { _ThrowIfSending(); if (keysEtc != null) { for (int i = 0; i < keysEtc.Length; i++) { var o = keysEtc[i].Value ?? ""; switch (o) { case string s: AddKeys(s); break; case clipboardData cd: AddClipboardData(cd); break; case KKey k: AddKey(k); break; case char c: AddChar(c); break; case int ms: AddSleep(ms); break; case Action g: AddAction(g); break; case KKeyScan t: AddKey(t.vk, t.scanCode, t.extendedKey); break; } } } return this; } /// /// Sends keys, text and executes other events added with the AddX functions. /// /// Don't clear the internal collection. If true, this function then can be called again (eg in loop) to send/execute the same keys etc. If false (default), clears the added keys etc; then you can call AddX functions and SendNow again. /// canSendAgain is true and keys_ end with + or (. /// Failed. For example there is no focused window when sending text. /// public void SendNow(bool canSendAgain = false) { //note: the "Now" in the name is just to make it different from the static function send(). If named Send, problems with DocFX etc. _ThrowIfSending(); if (_a.Count == 0) return; if (canSendAgain) { if (_pstate.paren || _pstate.plus) throw new ArgumentException("canSendAgain cannot be true if keys_ ends with + or ("); } //print.it("-- _parsing.mod --"); //print.it(_parsing.mod); _AddModUp(); //add mod-up events if need, eg Ctrl-up after "Ctrl+A" //print.it("-- _a --"); //print.it(_a); //perf.first(); int sleepFinally = 0; var bi = new inputBlocker() { ResendBlockedKeys = true }; try { _sending = true; _antiCapsLock = Options.NoCapsOff || !isCapsLock ? false : null; //print.it("{"); if (!Options.NoBlockInput) bi.Start(BIEvents.Keys); if (!Options.NoModOff) Internal_.ReleaseModAndDisableModMenu(); //perf.next(); for (int i = 0; i < _a.Count; i++) { var k = _a[i]; switch (k.Type) { case _KType.Sleep: if (i == _a.Count - 1) sleepFinally = k.sleep; else Internal_.Sleep(k.sleep); break; case _KType.Repeat: Debug.Assert(i > 0 && _a[i - 1].IsKeyOrChar); break; case _KType.Callback: (_GetData(k.data) as Action)(); break; case _KType.Char: _SendChar(k, i); break; case _KType.Text: _SendText(k); break; default: _SendKey(k, i); break; } } //perf.next(); sleepFinally += GetOptionsAndWndFocused_(getWndAlways: false).optk.SleepFinally; } finally { if (_antiCapsLock == true && !isCapsLock) Internal_.SendKey(KKey.CapsLock); _antiCapsLock = null; _sending = false; bi.Dispose(); //perf.nw(); //print.it("}"); //if canSendAgain, can be used like: AddX(); for(...) Send(); //else can be used like: AddX(); Send(); AddX(); Send(); if (!canSendAgain) { _a.Clear(); _data = null; _sstate.Clear(); //and don't clear _pstate } } if (sleepFinally > 0) Internal_.Sleep(sleepFinally); //_SyncWait(); //CONSIDER: instead of SleepFinally use TimeSyncFinally, default 100 ms. Eg send a sync key and wait max TimeSyncFinally ms. // Don't sync after each (or some) sent key. Usually it does not make sense. The final sync/sleep is useful if next statement is not an input function. //Sync problems: // Tried many ways, nothing is good enough. The test code now is in the "Unused" project. // The best would be non-LL keyboard hook that sets event when receives our sent special key-up. Especially when combined with 'get thread CPU usage' while waiting for the event. However these hooks don't work eg in Store apps. //Better add a Sync function (keys.sync) or/and special key name, let users do it explicitly where need. } #if !DEBUG /// /// Deprecated. Use . /// [EditorBrowsable(EditorBrowsableState.Never)] public void Send(bool canSendAgain = false) => SendNow(canSendAgain); #endif unsafe void _SendKey(_KEvent k, int i) { bool needScanCode = k.scan == 0 && !k.SIFlags.HasAny(_KFlags.Scancode | _KFlags.Unicode); var (optk, wFocus) = GetOptionsAndWndFocused_(getWndAlways: needScanCode); if (needScanCode) { var hkl = Api.GetKeyboardLayout(wFocus.ThreadId); //most layouts have the same standard scancodes, but eg dvorak different k.scan = Internal_.VkToSc(k.vk, hkl); } if (_antiCapsLock == null && !k.SIFlags.Has(_KFlags.Unicode) && k.vk is (>= KKey.A and <= KKey.Z) or (>= KKey.D0 and <= KKey.D9) or (>= KKey.OemSemicolon and <= KKey.OemTilde) or (>= KKey.OemOpenBrackets and <= KKey.OemQuotes) //CONSIDER: not if with a modifier ) _AntiCapsLock(); bool isLast = i == _a.Count - 1; _SendKey2(k, isLast ? default : _a[i + 1], isLast, optk); } //Caller should set k.scan; this func doesn't. unsafe static void _SendKey2(_KEvent k, _KEvent kNext, bool isLast, OKey optk) { var ki = new Api.INPUTK(k.vk, k.scan, (uint)k.SIFlags); int count = 1, sleep = optk.KeySpeed; if (isLast) { if (!k.IsPair) sleep = Internal_.LimitSleepTime(sleep) - optk.SleepFinally; } else { if (kNext.IsRepeat) count = kNext.repeat; else if (!k.IsPair) { //If this is pair, sleep between down and up, and don't sleep after up. //Else if repeat, sleep always. //Else in most cases don't need to sleep. In some cases need, but can limit the time. // For example, in Ctrl+C normally would not need to sleep after Ctrl down and Ctrl up. // However some apps/controls then may not work. Maybe they process mod and nonmod keys somehow async. // For example, Ctrl+C in IE address bar often does not work if there is no sleep after Ctrl down. Always works if 1 ms. sleep = Internal_.LimitSleepTime(sleep); if (kNext.IsKey) { bool thisMod = KeyTypes_.IsMod(k.vk), nextMod = KeyTypes_.IsMod(kNext.vk); if (!k.IsUp) { if (kNext.IsUp) sleep = optk.KeySpeed; else if (thisMod == nextMod) sleep = 0; } else { if (!thisMod || nextMod) sleep = 0; } } else if (kNext.IsSleep) sleep -= kNext.sleep; } } if (sleep < 0) sleep = 0; //var s = (k.vk).ToString(); //if (k.IsPair) print.it($"{s}<{sleep}>"); //else { var ud = k.IsUp ? '-' : '+'; if (sleep > 0) print.it($"{s}{ud} {sleep}"); else print.it($"{s}{ud}"); } for (int r = 0; r < count; r++) { //perf.first(); Api.SendInput(&ki); //perf.next(); if (sleep > 0) { Internal_.Sleep(sleep); } if (k.IsPair) { ki.dwFlags |= Api.KEYEVENTF_KEYUP; Api.SendInput(&ki); ki.dwFlags &= ~Api.KEYEVENTF_KEYUP; } //perf.nw(); //speed: min 400 mcs for each event. Often > 1000. Does not depend on whether all events sent by single SendInput call. } } unsafe void _SendChar(_KEvent ke, int i) { var (optk, wFocus) = GetOptionsAndWndFocused_(getWndAlways: true, requireFocus: true); nint hkl = Api.GetKeyboardLayout(wFocus.ThreadId); if (_antiCapsLock == null) _AntiCapsLock(); int count = 1; if (i < _a.Count - 1 && _a[i + 1].IsRepeat) count = _a[i + 1].repeat; int speed = optk.KeySpeed; if (count > 4) speed = Math.Min(speed, optk.TextSpeed + 2); KMod prevMod = 0; try { _SendChar2((char)ke.ch, OKeyText.KeysOrChar, speed, count, hkl, ref prevMod, false); //note: don't use optk.TextShiftEnter } finally { Internal_.ModPressRelease(false, prevMod); } } static unsafe void _SendChar2(char c, OKeyText textHow, int sleep, int count, nint hkl, ref KMod prevMod, bool shiftEnter) { KKey vk = 0; KMod mod = 0; if (c is '\n' or '\r') { //many apps don't support these as VK_PACKET vk = KKey.Enter; if (shiftEnter) mod = KMod.Shift; } else if (c is ' ' or '\t') { //some apps don't support these as VK_PACKET vk = (KKey)c; } else if (textHow != OKeyText.Characters) { (vk, mod) = _CharToKey(c, hkl); //print.it(c, vk, mod, (ushort)km); } if (vk == 0) { //use vk_packet if (prevMod != 0) { Internal_.ModPressRelease(false, prevMod); prevMod = 0; } //note: need key-up event for VK_PACKET too. // Known controls that need it: Qt edit controls; Office 2003 'type question' field. } else if (mod != prevMod) { var md = mod ^ prevMod; if (0 != (md & KMod.Ctrl)) Internal_.SendCtrl(0 != (mod & KMod.Ctrl)); if (0 != (md & KMod.Alt)) Internal_.SendAlt(0 != (mod & KMod.Alt)); if (0 != (md & KMod.Shift)) Internal_.SendShift(0 != (mod & KMod.Shift)); prevMod = mod; if (sleep > 0) Internal_.Sleep(Internal_.LimitSleepTime(sleep)); //need for apps that process mod-nonmod keys async } var ki = new _INPUTKEY2(vk, vk == 0 ? c : Internal_.VkToSc(vk, hkl), vk == 0 ? Api.KEYEVENTF_UNICODE : 0); for (int r = 0; r < count; r++) { Api.SendInput(&ki.k0, sleep > 0 ? 1 : 2); if (sleep > 0) { Internal_.Sleep(sleep); Api.SendInput(&ki.k1, 1); } } //rejected: try to synchronize somehow. To work better with slow and badly synchronized apps. //1. SendTimeout(WM_NULL). Although makes slower, usually does not make sync. //2. Sleep if the process uses CPU eg >50% of time. Tooo slow, even with Notepad. Tried GetProcessTimes and QueryProcessCycleTime (precise). //Eg UWP input processing is so slow and chaotic, impossible to sync. //rejected: option to sleep 1 ms every n-th char (eg use float 0...1 or negative value). Nothing good. } static (KKey vk, KMod mod) _CharToKey(char c, nint hkl) { short km = Api.VkKeyScanEx(c, hkl); //note: call for non-ASCII char too; depending on keyboard layout it can succeed if (0 != (km & 0xf800)) return default; //-1 if failed, mod flag 8 Hankaku key, 16/32 reserved for driver return ((KKey)(km & 0xff), (KMod)(km >> 8)); } unsafe void _SendText(_KEvent ke) { var (optk, wFocus) = GetOptionsAndWndFocused_(getWndAlways: true, requireFocus: true); object data = _GetData(ke.data); //string or clipboardData string s = data as string; OKeyText textHow; int flags = (byte)ke.vk; bool textHowSpecified = 0 != (flags & 0x80); //"^text" or AddText(string, OKeyText) if (textHowSpecified) textHow = (OKeyText)(flags & 0xf); else if (s != null && s.Length < optk.PasteLength) textHow = optk.TextHow; else textHow = OKeyText.Paste; if (textHow != OKeyText.Paste) { //use paste if there are Unicode surrogate pairs, because some apps/controls/frameworks don't support surrogates with WM_PACKET. //known apps that support: standard Edit and RichEdit controls, Chrome, Firefox, IE, WPF, WinForms, new Scintilla, Dreamweaver, LibreOffice. //known apps that don't: Office 2003, OpenOffice, old Scintilla. //known apps that don't if 0 sleep: QT edit controls in VirtualBox. //known apps that don't support these chars even when pasting: Java (tested the old and new frameworks). //tested: the same if SendInput(arrayOfAllChars). for (int i = 0; i < s.Length; i++) if ((s[i] & 0xf800) == 0xd800) { textHow = OKeyText.Paste; break; } } nint hkl = 0; if (textHow is OKeyText.KeysOrChar or OKeyText.KeysOrPaste) { hkl = Api.GetKeyboardLayout(wFocus.ThreadId); if (textHow == OKeyText.KeysOrPaste) { foreach (char c in s) { if (c is '\r' or '\n') continue; if (_CharToKey(c, hkl).vk == default) { textHow = OKeyText.Paste; break; } } } } //print.it(optk.TextHow, textHow); if (textHow == OKeyText.Paste) { Pasting?.Invoke(this, new PastingEventArgs { Text = s, Options = optk, WndFocus = wFocus }); clipboard.Paste_(data, optk, wFocus); return; } if (_antiCapsLock == null && textHow is OKeyText.KeysOrChar or OKeyText.KeysOrPaste) _AntiCapsLock(); KMod prevMod = 0; int sleep = 0 != (flags & 0x40) ? optk.KeySpeed : optk.TextSpeed; //0x40 if ^text try { for (int i = 0; i < s.Length; i++) { char c = s[i]; if (c == '\r' && s.Eq(i + 1, '\n')) continue; //\r\n -> \n -> key Enter _SendChar2(c, textHow, sleep, 1, hkl, ref prevMod, textHowSpecified ? false : optk.TextShiftEnter); } } finally { Internal_.ModPressRelease(false, prevMod); } //rejected: throw if changed the focused window. // Possible false positives, because everything is async. } void _AntiCapsLock(/*OKey optk*/) { //if (/*&& !optk.NoCapsOff*/) { if (!isCapsLock) { _antiCapsLock = false; return; } if (isPressed(KKey.CapsLock)) Internal_.SendKey(KKey.CapsLock, false); //never mind: in this case later may not restore CapsLock because of auto-repeat Internal_.SendKey(KKey.CapsLock, true); bool ok = isPressed(KKey.CapsLock); //the send can fail because of UAC or the Windows setting Internal_.SendKey(KKey.CapsLock, false); //note: don't call isCapsLock again here. It is unreliable because GetKeyState is sync. // Eg in some cases ignores the new key state until this UI thread removes all messages from queue. if (!ok && IsCapsLockShiftOff_()) { //Shift is set to turn off CapsLock in Settings > Time & Language > Language > Keyboard > Input method > Hot keys. WindowsHook.IgnoreLShiftCaps_(2000); Internal_.SendKey(KKey.Shift); WindowsHook.IgnoreLShiftCaps_(0); //note: need IgnoreLShiftCaps_, because when we send Shift, the BlockInput hook receives these events: //Left Shift down, not injected //!! //Caps Lock down, not injected //Caps Lock up, not injected //Left Shift up, injected //speed: often ~15 ms. Without Shift max 5 ms. } _antiCapsLock = true; //} //note: don't make _restoreCapsLock false if still isCapsLock true, because isCapsLock unreliable. // If SendKey(CapsLock) did not work now, it probably will not work afterwards. //CONSIDER: remove this feature, or set non-default. // Instead, when sending text as keys, if CapsLock, invert Shift. Eg PAD uses this. // But what then should do when sending keys (not text)? Probably should ignore CapsLock. // Probably safer with CapsLock off. Eg some target apps may interpret text differently when with an unexpected Shift. } /// /// Returns true if Shift is set to turn off CapsLock (system setting). /// internal static bool IsCapsLockShiftOff_() => s_isCapsLockShiftOff ??= Microsoft.Win32.Registry.GetValue(@"HKEY_CURRENT_USER\Keyboard Layout", "Attributes", 0) is int r1 && 0 != (r1 & 0x10000); static bool? s_isCapsLockShiftOff; /// /// Before pasting text through clipboard. /// public event EventHandler Pasting; } ================================================ FILE: Au/Input/keys.more.cs ================================================ namespace Au; partial class keys { /// /// Miscellaneous rarely used keyboard-related functions. /// public static partial class more { /// /// Converts key name to . /// /// 0 if unknown key name. /// [Key name](xref:key_names). public static KKey parseKeyName(string keyName) { keyName ??= ""; return _KeynameToKey(keyName, 0, keyName.Length); } /// /// Calls and throws if invalid key string. /// internal static KKey ParseKeyNameThrow_(string keyName) { var k = parseKeyName(keyName); if (k == 0) throw new ArgumentException("Unknown key name or error in key string."); return k; } /// /// Converts key name to . /// /// 0 if unknown key name. /// String containing [key name](xref:key_names). /// Key name start index in s. /// Key name length. /// Invalid startIndex or length. public static KKey parseKeyName(string s, int startIndex, int length) { s ??= ""; if ((uint)startIndex > s.Length || (uint)length > s.Length - startIndex) throw new ArgumentOutOfRangeException(); return _KeynameToKey(s, startIndex, length); } /// /// Converts keys string to array. /// /// String containing one or more [key names](xref:key_names). Operators are not supported. /// Error in keys_ string. public static KKey[] parseKeysString(string keys_) { var a = new List(); foreach (var g in _SplitKeysString(keys_ ?? "")) { KKey k = _KeynameToKey(keys_, g.Start, g.Length); if (k == 0) throw _ArgumentException_ErrorInKeysString(keys_, g.Start, g.Length); a.Add(k); } return a.ToArray(); } /// /// Converts hotkey string like "Ctrl+A" to and . /// /// false if the string is invalid. /// /// For example, if s is "Ctrl+Left", sets mod = KMod.Ctrl, key = KKey.Left. /// /// [Key names](xref:key_names) are like with . /// /// Must be single non-modifier key, preceded by zero or more of modifier keys Ctrl, Shift, Alt, Win, all joined with +. /// Valid hotkey examples: "A", "a", "7", "F12", ".", "End", "Ctrl+D", "Ctrl+Alt+Shift+Win+Left", " Ctrl + U ". /// Invalid hotkey examples: null, "", "A+B", "Ctrl+A+K", "A+Ctrl", "Ctrl+Shift", "Ctrl+", "NoSuchKey", "tab". /// public static bool parseHotkeyString(string s, out KMod mod, out KKey key) { key = 0; mod = 0; if (s == null) return false; int i = 0; foreach (var g in _SplitKeysString(s)) { if (key != 0) return false; if ((i++ & 1) == 0) { KKey k = _KeynameToKey(s, g.Start, g.Length); if (k == 0) return false; var m = Internal_.KeyToMod(k); if (m != 0) { if ((m & mod) != 0) return false; mod |= m; } else key = k; } else if (g.Length != 1 || s[g.Start] != '+') return false; } return key != 0 && key != KKey.Packet; } /// /// Converts hotkey string like "Ctrl+A" to winforms . /// /// /// For example, if s is "Ctrl+Left", sets hotkey = Keys.Control | Keys.Left. /// /// false if the string is invalid or contains "Win". public static bool parseHotkeyString(string s, out System.Windows.Forms.Keys hotkey) { if (!parseHotkeyString(s, out var m, out var k)) { hotkey = 0; return false; } hotkey = KModToWinforms(m) | (System.Windows.Forms.Keys)k; if (m.Has(KMod.Win)) return false; return true; //return Enum.IsDefined(typeof(System.Windows.Forms.Keys), (System.Windows.Forms.Keys)k); //not too slow //tested: enum Keys has all KKey values + some extinct. } /// /// Converts hotkey string like "Ctrl+A" to WPF and or . /// /// /// For example, if s is "Ctrl+Left", sets mod = ModifierKeys.Control and key = Key.Left. /// /// Supported mouse button strings: "Click", "D-click", "R-click", "M-click", "Wheel". Example: "Ctrl+R-click". The first character of a mouse word is case-insensitive. /// /// false if the string is invalid or contains incorrectly specified mouse buttons. public static bool parseHotkeyString(string s, out System.Windows.Input.ModifierKeys mod, out System.Windows.Input.Key key, out System.Windows.Input.MouseAction mouse) { mod = 0; key = 0; mouse = 0; if (s.Ends("lick") || s.Ends("heel")) { int i = s.LastIndexOf('+') + 1; var v = s.AsSpan(i); var co = StringComparison.OrdinalIgnoreCase; if (v.Equals("Click", co)) mouse = System.Windows.Input.MouseAction.LeftClick; else if (v.Equals("D-click", co)) mouse = System.Windows.Input.MouseAction.LeftDoubleClick; else if (v.Equals("R-click", co)) mouse = System.Windows.Input.MouseAction.RightClick; else if (v.Equals("M-click", co)) mouse = System.Windows.Input.MouseAction.MiddleClick; else if (v.Equals("Wheel", co)) mouse = System.Windows.Input.MouseAction.WheelClick; if (mouse != default) { if (i == 0) return true; s = s.ReplaceAt(i.., "A"); //replace the mouse word with a key name, else can't parse } } if (!parseHotkeyString(s, out var m, out var k)) return false; mod = KModToWpf(m); return mouse != default || (key = KKeyToWpf(k)) != default; //tested: enum Key has all KKey values except mouse buttons and packet. } /// /// Parses hotkey trigger string or mouse trigger modifiers string. /// Like , but supports "any mod" (like "Shift?+K" or "?+K") and noKey. /// /// /// /// /// /// Modifiers only. If true, s must be "modifiers" or null/"". If false, s must be "key" or "modifiers+key". public static bool parseTriggerString(string s, out KMod mod, out KMod modAny, out KKey key, bool noKey) { key = 0; mod = 0; modAny = 0; if (s.NE()) return noKey; int i = 0; bool ignore = false; foreach (var g in _SplitKeysString(s)) { if (ignore) { ignore = false; continue; } if (key != 0) return false; if ((i++ & 1) == 0) { KKey k = _KeynameToKey(s, g.Start, g.Length); if (k == 0) return false; var m = Internal_.KeyToMod(k); if (m != 0) { if ((m & (mod | modAny)) != 0) return false; if (ignore = g.End < s.Length && s[g.End] == '?') modAny |= m; //eg "Shift?+K" else mod |= m; } else { if (i == 1 && g.Length == 1 && s[g.Start] == '?') modAny = (KMod)15; //eg "?+K" else key = k; } } else if (g.Length != 1 || s[g.Start] != '+') return false; } if (noKey) return (mod | modAny) != 0 && key == 0; return key != 0; } /// /// Formats hotkey string like "Ctrl+Shift+K". /// /// Append to this StringBuilder. /// /// public static void hotkeyToString(StringBuilder b, KMod mod, KKey key) { if (mod.Has(KMod.Ctrl)) b.Append("Ctrl+"); if (mod.Has(KMod.Alt)) b.Append("Alt+"); if (mod.Has(KMod.Shift)) b.Append("Shift+"); if (mod.Has(KMod.Win)) b.Append("Win+"); b.Append(keyToString(key)).ToString(); } /// /// Formats hotkey string like "Ctrl+Shift+K". /// public static string hotkeyToString(KMod mod, KKey key) { if (mod == 0) return keyToString(key); using (new StringBuilder_(out var b)) { hotkeyToString(b, mod, key); return b.ToString(); } } /// /// Gets [key name](xref:key_names) that can be used in keys strings with etc. /// public static string keyToString(KKey key) => c_keyNames[(int)key] ?? ("VK" + (int)key); static readonly string[] c_keyNames = { "", "MouseLeft", "MouseRight", "Break", "MouseMiddle", "MouseX1", "MouseX2", null, "Back", "Tab", null, null, "Clear", "Enter", null, null, "Shift", "Ctrl", "Alt", "Pause", "CapsLock", "IMEKanaMode", null, "IMEJunjaMode", "IMEFinalMode", "IMEKanjiMode", null, "Esc", "IMEConvert", "IMENonconvert", "IMEAccept", "IMEModeChange", "Space", "PgUp", "PgDn", "End", "Home", "Left", "Up", "Right", "Down", null, null, null, "PrtSc", "Ins", "Del", null, "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", null, null, null, null, null, null, null, "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "Win", "RWin", "Apps", null, "Sleep", "#0", "#1", "#2", "#3", "#4", "#5", "#6", "#7", "#8", "#9", "#*", "#+", null, "#-", "#.", "#/", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "F13", "F14", "F15", "F16", "F17", "F18", "F19", "F20", "F21", "F22", "F23", "F24", null, null, null, null, null, null, null, null, "NumLock", "ScrollLock", null, null, null, null, null, null, null, null, null, null, null, null, null, null, "LShift", "RShift", "LCtrl", "RCtrl", "LAlt", "RAlt", "BrowserBack", "BrowserForward", "BrowserRefresh", "BrowserStop", "BrowserSearch", "BrowserFavorites", "BrowserHome", "VolumeMute", "VolumeDown", "VolumeUp", "MediaNextTrack", "MediaPrevTrack", "MediaStop", "MediaPlayPause", "LaunchMail", "LaunchMediaSelect", "LaunchApp1", "LaunchApp2", null, null, ";", "=", ",", "-", ".", "/", "`", null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "[", "|", "]", "'", null, null, null, null, null, null, "IMEProcessKey", null, "Packet", null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, }; //this script creates the array: /* print.clear(); var b=new StringBuilder("static readonly string[] c_keyNames = {"); for (int i = 0; i < 256; i++) { var k=(KKey)i; var s=k switch { 0 => "", >= KKey.D0 and <= KKey.D9 => ((char)k).ToString(), >= KKey.NumPad0 and <= KKey.NumPad9 => "#" + ((char)(k-KKey.NumPad0+'0')).ToString(), KKey.OemMinus => "-", KKey.OemPlus => "=", KKey.OemTilde => "`", KKey.OemOpenBrackets => "[", KKey.OemCloseBrackets => "]", KKey.OemPipe => "|", KKey.OemSemicolon => ";", KKey.OemQuotes => "'", KKey.OemComma => ",", KKey.OemPeriod => ".", KKey.OemQuestion => "/", KKey.Decimal => "#.", KKey.Add => "#+", KKey.Divide => "#/", KKey.Multiply => "#*", KKey.Subtract => "#-", KKey.Escape => "Esc", KKey.PageUp => "PgUp", KKey.PageDown => "PgDn", KKey.PrintScreen => "PrtSc", KKey.Insert => "Ins", KKey.Delete => "Del", _ => Enum.IsDefined(k) ? k.ToString() : null }; // print.it(k, s); b.Append("\r\n\t"); if(s==null) b.Append("null"); else b.Append('"').Append(s).Append('"'); b.Append(','); } b.Append("\r\n};"); print.it(b.ToString()); */ /// /// Converts modifier key flags from KMod to winforms Keys. /// /// /// For Win returns flag (Keys)0x80000. /// public static System.Windows.Forms.Keys KModToWinforms(KMod mod) => (System.Windows.Forms.Keys)((int)mod << 16); /// /// Converts modifier key flags from winforms Keys to KMod. /// /// /// For Win can be used flag (Keys)0x80000. /// public static KMod KModFromWinforms(System.Windows.Forms.Keys mod) => (KMod)((int)mod >> 16); /// /// Converts modifier key flags from KMod to WPF ModifierKeys. /// public static System.Windows.Input.ModifierKeys KModToWpf(KMod mod) => (System.Windows.Input.ModifierKeys)_SwapMod((int)mod); /// /// Converts modifier key flags from WPF ModifierKeys to KMod. /// public static KMod KModFromWpf(System.Windows.Input.ModifierKeys mod) => (KMod)_SwapMod((int)mod); static int _SwapMod(int m) => (m & 0b1010) | (m << 2 & 4) | (m >> 2 & 1); /// /// Converts key from KKey to WPF Key. /// public static System.Windows.Input.Key KKeyToWpf(KKey k) => System.Windows.Input.KeyInterop.KeyFromVirtualKey((int)k); /// /// Converts key from WPF Key to KKey. /// public static KKey KKeyFromWpf(System.Windows.Input.Key k) => (KKey)System.Windows.Input.KeyInterop.VirtualKeyFromKey(k); /// /// Sends single key. /// /// Virtual-key code. /// true down, false up, null down and up. /// /// Keyboard layout handle for scan code. See API GetKeyboardLayout. /// If 0 (default), uses keyboard layout of this thread; don't use 0 for keys whose scancode depends on keyboard layout. /// If -1, uses keyboard layout of the focused or active window. /// /// An "extra info" value that can be used for example by keyboard hooks to recognize the key sender. If null (default), uses the same value as other functions of this library. /// Don't throw exception. /// /// This is a low-level function. Does nothing more (sleep, block input, etc). Does not use options. Just gets missing info (scan code etc) and calls API SendInput. /// /// public static void sendKey(KKey k, bool? down = null, nint hkl = 0, int? extra = null, bool dontThrow = false) => Internal_.SendKey(k, down, hkl, extra, dontThrow); //FUTURE: RemapKeyboardKeys. See QM2. } } ================================================ FILE: Au/Input/keys_static.cs ================================================ namespace Au; public partial class keys { #region get key state /// /// Gets key states for using in UI code (winforms, WPF, etc). /// /// /// Use functions of this class in user interface code (winforms, WPF, etc). In other code (automation scripts, etc) usually it's better to use functions of class. /// /// In Windows there are two API to get key state - GetKeyState and GetAsyncKeyState. /// /// API GetAsyncKeyState is used by class and not by this class (keys.gui). When physical key state changes (pressed/released), GetAsyncKeyState sees the change immediately. It is good in automation scripts, but not good in UI code because the state is not synchronized with the message queue. /// /// This class (keys.gui) uses API GetKeyState. In the foreground thread (of the active window), it sees key state changes not immediately but after the thread reads key messages from its queue. It is good in UI threads. In background threads this API usually works like GetAsyncKeyState, but it depends on API AttachThreadInput and in some cases is less reliable, for example may be unaware of keys pressed before the thread started. /// /// The key state returned by these API is not always the same as of the physical keyboard. There is no API to get real physical state. Some cases when it is different: /// 1. The key is pressed or released by software, such as the function of this library. /// 2. The key is blocked by a low-level hook. For example, hotkey triggers of this library use hooks. /// 3. The foreground window belongs to a process with higher UAC integrity level. /// /// Also there is API GetKeyboardState. It gets states of all keys in single call. Works like GetKeyState. /// public static class gui { //rejected: instead of class keys.gui add property keys.isUIThread. If true, let its functions work like now keys.gui. /// /// Calls API GetKeyState and returns its return value. /// /// /// If returns < 0, the key is pressed. If the low-order bit is 1, the key is toggled; it works only with CapsLock, NumLock, ScrollLock and several other keys, as well as mouse buttons. /// Can be used for mouse buttons too, for example keys.gui.getKeyState(KKey.MouseLeft). When mouse left and right buttons are swapped, gets logical state, not physical. /// public static short getKeyState(KKey key) => Api.GetKeyState((int)key); /// /// Returns true if the specified key or mouse button is pressed. /// /// /// Can be used for mouse buttons too. Example: keys.gui.isPressed(KKey.MouseLeft). When mouse left and right buttons are swapped, gets logical state, not physical. /// public static bool isPressed(KKey key) => getKeyState(key) < 0; /// /// Returns true if the specified key or mouse button is toggled. /// /// /// Works only with CapsLock, NumLock, ScrollLock and several other keys, as well as mouse buttons. /// public static bool isToggled(KKey key) => 0 != (getKeyState(key) & 1); /// /// Returns true if the Alt key is pressed. /// public static bool isAlt => isPressed(KKey.Alt); /// /// Returns true if the Ctrl key is pressed. /// public static bool isCtrl => isPressed(KKey.Ctrl); /// /// Returns true if the Shift key is pressed. /// public static bool isShift => isPressed(KKey.Shift); /// /// Returns true if the Win key is pressed. /// public static bool isWin => isPressed(KKey.Win) || isPressed(KKey.RWin); /// /// Returns true if some modifier keys are pressed. /// /// Return true if some of these keys are pressed. Default: Ctrl, Shift or Alt. /// /// By default does not check the Win key, as it is not used in UI, but you can include it in mod if need. /// public static bool isMod(KMod mod = KMod.Ctrl | KMod.Shift | KMod.Alt) { if (0 != (mod & KMod.Ctrl) && isCtrl) return true; if (0 != (mod & KMod.Shift) && isShift) return true; if (0 != (mod & KMod.Alt) && isAlt) return true; if (0 != (mod & KMod.Win) && isWin) return true; return false; } /// /// Gets flags indicating which modifier keys are pressed. /// /// Check only these keys. Default: Ctrl, Shift, Alt. /// /// By default does not check the Win key, as it is not used in UI, but you can include it in mod if need. /// public static KMod getMod(KMod mod = KMod.Ctrl | KMod.Shift | KMod.Alt) { KMod R = 0; if (0 != (mod & KMod.Ctrl) && isCtrl) R |= KMod.Ctrl; if (0 != (mod & KMod.Shift) && isShift) R |= KMod.Shift; if (0 != (mod & KMod.Alt) && isAlt) R |= KMod.Alt; if (0 != (mod & KMod.Win) && isWin) R |= KMod.Win; return R; } /// /// Returns true if the CapsLock key is toggled. /// /// /// The same as . /// public static bool isCapsLock => isToggled(KKey.CapsLock); /// /// Returns true if the NumLock key is toggled. /// /// /// The same as . /// public static bool isNumLock => isToggled(KKey.NumLock); /// /// Returns true if the ScrollLock key is toggled. /// /// /// The same as . /// public static bool isScrollLock => isToggled(KKey.ScrollLock); } /// /// Returns true if the specified key or mouse button is pressed. /// In UI code use instead. /// /// /// Uses API GetAsyncKeyState. /// public static bool isPressed(KKey key) { if ((key == KKey.MouseLeft || key == KKey.MouseRight) && 0 != Api.GetSystemMetrics(Api.SM_SWAPBUTTON)) key = (KKey)((int)key ^ 3); //makes this func 3 times slower, eg 2 -> 6 mcs when cold CPU. But much faster when called next time without a delay; for example mouse.isPressed(Left|Right) is not slower than mouse.isPressed(Left), although calls this func 2 times. return Api.GetAsyncKeyState((int)key) < 0; } /// /// Returns true if the Alt key is pressed. Calls . /// In UI code use instead. /// public static bool isAlt => isPressed(KKey.Alt); /// /// Returns true if the Ctrl key is pressed. Calls . /// In UI code use instead. /// public static bool isCtrl => isPressed(KKey.Ctrl); /// /// Returns true if the Shift key is pressed. Calls . /// In UI code use instead. /// public static bool isShift => isPressed(KKey.Shift); /// /// Returns true if the Win key is pressed. Calls . /// In UI code use instead. /// public static bool isWin => isPressed(KKey.Win) || isPressed(KKey.RWin); /// /// Returns true if some modifier keys are pressed: Ctrl, Shift, Alt, Win. Calls . /// In UI code use instead. /// /// Return true if some of these keys are pressed. Default - any. /// public static bool isMod(KMod mod = KMod.Ctrl | KMod.Shift | KMod.Alt | KMod.Win) { if (0 != (mod & KMod.Ctrl) && isCtrl) return true; if (0 != (mod & KMod.Shift) && isShift) return true; if (0 != (mod & KMod.Alt) && isAlt) return true; if (0 != (mod & KMod.Win) && isWin) return true; return false; } /// /// Gets flags indicating which modifier keys are pressed: Ctrl, Shift, Alt, Win. Calls . /// In UI code use instead. /// /// Check only these keys. Default - all four. public static KMod getMod(KMod mod = KMod.Ctrl | KMod.Shift | KMod.Alt | KMod.Win) { KMod R = 0; if (0 != (mod & KMod.Ctrl) && isCtrl) R |= KMod.Ctrl; if (0 != (mod & KMod.Shift) && isShift) R |= KMod.Shift; if (0 != (mod & KMod.Alt) && isAlt) R |= KMod.Alt; if (0 != (mod & KMod.Win) && isWin) R |= KMod.Win; return R; } /// /// Returns true if the CapsLock key is toggled. /// public static bool isCapsLock => gui.isCapsLock; /// /// Returns true if the NumLock key is toggled. /// public static bool isNumLock => gui.isNumLock; /// /// Returns true if the ScrollLock key is toggled. /// public static bool isScrollLock => gui.isScrollLock; #endregion #region wait /// /// Waits while some modifier keys (Ctrl, Shift, Alt, Win) are pressed. See . /// /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// Check only these keys. Default: all. /// Returns true. On timeout returns false if timeout is negative; else exception. /// timeout time has expired (if > 0). public static bool waitForNoModifierKeys(Seconds timeout = default, KMod mod = KMod.Ctrl | KMod.Shift | KMod.Alt | KMod.Win) { return waitForNoModifierKeysAndMouseButtons(timeout, mod, 0); } /// /// Waits while some modifier keys (Ctrl, Shift, Alt, Win) or mouse buttons are pressed. /// /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). Default 0. /// Check only these keys. Default: all. /// Check only these buttons. Default: all. /// Returns true. On timeout returns false if timeout is negative; else exception. /// timeout time has expired (if > 0). /// /// /// public static bool waitForNoModifierKeysAndMouseButtons(Seconds timeout = default, KMod mod = KMod.Ctrl | KMod.Shift | KMod.Alt | KMod.Win, MButtons buttons = MButtons.Left | MButtons.Right | MButtons.Middle | MButtons.X1 | MButtons.X2) { var loop = new WaitLoop(timeout); for (; ; ) { if (!isMod(mod) && !mouse.isPressed(buttons)) return true; if (!loop.Sleep()) return false; } } /// /// Waits while the specified keys or/and mouse buttons are pressed. /// /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// One or more keys or/and mouse buttons. Waits until all are released. /// Returns true. On timeout returns false if timeout is negative; else exception. /// timeout time has expired (if > 0). public static bool waitForReleased(Seconds timeout, params KKey[] keys_) { return wait.until(timeout, () => { foreach (var k in keys_) if (isPressed(k)) return false; return true; }); } /// /// Waits while the specified keys are pressed. /// /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// One or more [keys](xref:key_names) without operators. Waits until all are released. /// Returns true. On timeout returns false if timeout is negative; else exception. /// Error in keys_ string. /// timeout time has expired (if > 0). public static bool waitForReleased(Seconds timeout, string keys_) { return waitForReleased(timeout, more.parseKeysString(keys_)); } /// /// Waits for key-down or key-up event of the specified key. /// /// Returns true. On timeout returns false if timeout is negative; else exception. /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// Wait for this key. /// Wait for key-up event. /// Make the event invisible to other apps. If up is true, makes the down event invisible too, if it comes while waiting for the up event. /// key is 0. /// timeout time has expired (if > 0). /// /// Waits for key event, not for key state. /// Uses low-level keyboard hook. Can wait for any single key. See also . /// Ignores key events injected by functions of this library. /// /// /// /// public static bool waitForKey(Seconds timeout, KKey key, bool up = false, bool block = false) { if (key == 0) throw new ArgumentException(); return 0 != _WaitForKey(timeout, key, up, block); } /// Wait for this key. A single-key string. See [key names](xref:key_names). /// Invalid key string. /// /// /// /// public static bool waitForKey(Seconds timeout, string key, bool up = false, bool block = false) { return 0 != _WaitForKey(timeout, more.ParseKeyNameThrow_(key), up, block); } /// /// Waits for key-down or key-up event of any key, and gets the key code. /// /// /// Returns the key code. On timeout returns 0 if timeout is negative; else exception. /// For modifier keys returns the left or right key code, for example LCtrl/RCtrl, not Ctrl. /// /// timeout time has expired (if > 0). /// /// /// /// public static KKey waitForKey(Seconds timeout, bool up = false, bool block = false) { return _WaitForKey(timeout, 0, up, block); } static KKey _WaitForKey(Seconds timeout, KKey key, bool up, bool block) { //TODO3: if up and block: don't block if was down when starting to wait. Also in the Mouse func. KKey R = 0; using (WindowsHook.Keyboard(x => { if (key != 0 && !x.IsKey(key)) return; if (x.IsUp != up) { if (up && block) { //key down when waiting for up. If block, now block down too. if (key == 0) key = x.vkCode; x.BlockEvent(); } return; } R = x.vkCode; //info: for mod keys returns left/right if (block) x.BlockEvent(); })) wait.doEventsUntil(timeout, () => R != 0); return R; } /// /// Waits for keyboard events using callback function. /// /// /// Returns the key code. On timeout returns 0 if timeout is negative; else exception. /// For modifier keys returns the left or right key code, for example LCtrl/RCtrl, not Ctrl. /// /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// Callback function that receives key down and up events. Let it return true to stop waiting. /// Make the key down event invisible to other apps (when the callback function returns true). /// /// Waits for key event, not for key state. /// Uses low-level keyboard hook. /// Ignores key events injected by functions of this library. /// /// /// Wait for F3 or Esc. /// !k.IsUp && k.Key is KKey.F3 or KKey.Escape, block: true); /// print.it(k); /// ]]> /// public static KKey waitForKeys(Seconds timeout, Func f, bool block = false) { KKey R = 0; using (WindowsHook.Keyboard(x => { if (!f(x)) return; R = x.vkCode; //info: for mod keys returns left/right if (block && !x.IsUp) x.BlockEvent(); })) wait.doEventsUntil(timeout, () => R != 0); return R; } //CONSIDER: Same for mouse. /// /// Registers a temporary hotkey and waits for it. /// /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// Hotkey. Can be: string like "Ctrl+Shift+Alt+Win+K", tuple (KMod, KKey), enum , enum Keys, struct . /// Also wait until hotkey modifier keys released. /// Returns true. On timeout returns false if timeout is negative; else exception. /// Error in hotkey string. /// Failed to register hotkey. /// timeout time has expired (if > 0). /// /// Uses (API RegisterHotKey). /// Fails if the hotkey is currently registered by this or another application or used by Windows. /// Most single-key and Shift+key hotkeys don't work when the active window has higher UAC integrity level than this process. Media keys may work. /// /// /// /// public static bool waitForHotkey(Seconds timeout, [ParamString(PSFormat.Hotkey)] KHotkey hotkey, bool waitModReleased = false) { if (s_atomWFH == 0) s_atomWFH = Api.GlobalAddAtom("Au.WaitForHotkey"); using (RegisteredHotkey rhk = default) { if (!rhk.Register(s_atomWFH, hotkey)) throw new AuException(0, "*register hotkey"); if (!wait.forPostedMessage(timeout, (ref MSG m) => m.message == Api.WM_HOTKEY && m.wParam == s_atomWFH)) return false; } if (waitModReleased) waitForNoModifierKeys(0, hotkey.Mod); return true; } static ushort s_atomWFH; /// /// Sets a temporary keyboard hook and waits for a hotkey. /// /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// One or more hotkeys. Examples: ["Ctrl+M"], ["Win+M", "Ctrl+Shift+Left"]. /// Make the key down event of the non-modifier key invisible to other apps. Default true. /// Also wait until hotkey modifier keys released. /// 1-based index of the element in the array of hotkeys. On timeout returns 0 (if timeout negative; else exception). /// /// /// Uses . /// Works even if the hotkey is used by Windows (except Win+L and Ctrl+Alt+Del) or an app as a hotkey or trigger. /// Does not work when the active window has higher UAC integrity level than this process. /// /// /// /// public static int waitForHotkeys(Seconds timeout, [ParamString(PSFormat.Hotkey)] KHotkey[] hotkeys, bool block = true, bool waitModReleased = false) { int R = 0; keys.waitForKeys(timeout, k => { if (!k.IsUp && k.Mod == 0) { var mod = keys.getMod(); for (int i = 0; i < hotkeys.Length; i++) { if (k.Key == hotkeys[i].Key && mod == hotkeys[i].Mod) { R = i + 1; return true; } } } return false; }, block); if (waitModReleased && R > 0) keys.waitForNoModifierKeys(0, hotkeys[R - 1].Mod); return R; } #endregion /// /// Generates virtual keystrokes (keys, text). /// /// /// Arguments of these types: ///
• string - [key names and operators](xref:key_names), like "Enter A Ctrl+A".\ /// Tool: in "" string press Ctrl+Space. ///
• string with prefix "!" - literal text.\ /// Example: var p = "pass"; keys.send("!user", "Tab", "!" + p, "Enter"); ///
• string with prefix "%" - HTML to paste. Full or fragment. ///
- clipboard data to paste. ///
- a single key.\ /// Example: keys.send("Shift+", KKey.Left, "*3"); is the same as keys.send("Shift+Left*3"); ///
int - sleep milliseconds. Max 10000.\ /// Example: keys.send("Left", 500, "Right"); ///
- callback function.\ /// Example: Action click = () => mouse.click(); keys.send("Shift+", click); ///
- a single key, specified using scan code and/or virtual-key code and extended-key flag.\ /// Example: keys.send(new KKeyScan(0x3B, false)); //key F1\ /// Example: keys.send(new KKeyScan(KKey.Enter, true)); //numpad Enter ///
char - a single character. Like text with or operator ^. /// /// An invalid value, for example an unknown key name. /// Failed. When sending text, fails if there is no focused window. /// /// /// String syntax: [key names and operators](xref:key_names). /// /// Uses : /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// ///
OptionDefaultChanged
false. /// Blocks user-pressed keys. Sends them afterwards. ///
If the last argument is "sleep", stops blocking before executing it; else stops blocking after executing all arguments.
true. /// Does not block user-pressed keys.
false. /// If the CapsLock key is toggled, untoggles it temporarily (presses it before and after).true. /// Does not touch the CapsLock key. ///
Alphabetic keys of "keys" arguments can depend on CapsLock. Text of "text" arguments doesn't depend on CapsLock, unless is KeysX.
false. /// Releases modifier keys (Alt, Ctrl, Shift, Win). ///
Does it only at the start; later they cannot interfere, unless is true.
true. /// Does not touch modifier keys.
0 ms.0 - 1000. /// Changes the speed for "text" arguments.
2 ms.0 - 1000. /// Changes the speed for "keys" arguments.
5 ms.0 - 1000. /// Changes the speed of Ctrl+V keys when pasting text or HTML using clipboard.
10 ms.0 - 10000. ///
Tip: to sleep finally, also can be used code like this: keys.send("keys", 1000);.
.KeysOrChar, KeysOrPaste or Paste.
false.true. When sending text, instead of Enter send Shift+Enter.
200. ///
This option is used for "text" arguments. If text length >= this value, uses clipboard.
>=0.
false. ///
This option is used for "text" arguments when using clipboard. ///
true.
true. /// Restore clipboard data (by default only text). ///
This option is used for "text" and "HTML" arguments when using clipboard.
false. /// Don't restore clipboard data.
null.Callback function that can modify options depending on active window etc.
/// /// This function does not wait until the target app receives and processes sent keystrokes and text; there is no reliable way to know it. It just adds small delays depending on options ( etc). If need, change options or add "sleep" arguments or wait after calling this function. Sending text through the clipboard normally does not have these problems. /// /// Don't use this function to automate windows of own thread. Call it from another thread. See example with async/await. /// /// Administrator and uiAccess processes don't receive keystrokes sent by standard user processes. See [](xref:uac). /// /// Mouse button codes/names (eg ) cannot be used to click. Instead use callback, like in the "Ctrl+click" example. /// /// You can use a variable instead of this function. Example: new keys(null).Add("keys", "!text").SendNow();. More examples in topic. /// /// This function calls , which calls these functions depending on argument type: , , , , , , , . Then calls . /// /// Uses API SendInput. ///
/// /// mouse.click(); /// keys.send("Ctrl+", click); /// /// //Ctrl+click /// keys.send("Ctrl+", new Action(() => mouse.click())); /// ]]> /// Show window and send keys/text to it when button clicked. /// { /// //keys.send("Tab", "!text", 2000, "Esc"); //no /// await Task.Run(() => { keys.send("Tab", "!text", 2000, "Esc"); }); //use other thread /// }); /// b.R.Add("Text", out TextBox text1); /// b.R.AddOkCancel(); /// b.End(); /// if (!b.ShowDialog()) return; /// ]]> /// public static void send([ParamString(PSFormat.Keys)] params KKeysEtc[] keysEtc) { new keys(opt.key).Add(keysEtc).SendNow(); } //CONSIDER: move most of Remarks to Articles. Also make the param doc smaller, and move the big list to Remarks. /// /// Generates virtual keystrokes. Like , but without reliability features: delays, user input blocking, resetting modifiers/CapsLock. /// /// /// Ignores and instead uses default options with these changes: /// - SleepFinally = 0. /// - KeySpeed = 0. /// - NoBlockInput = true. /// - NoCapsOff = true. /// - NoModOff = true. /// /// /// public static void sendL([ParamString(PSFormat.Keys)] params KKeysEtc[] keysEtc) { var o = new OKey() { KeySpeed = 0, NoBlockInput = true, NoCapsOff = true, NoModOff = true, SleepFinally = 0 }; new keys(o).Add(keysEtc).SendNow(); } /// /// Sends text to the active window, using virtual keystrokes or clipboard. /// /// Text. Can be null. /// /// HTML. Can be full HTML or fragment. See . /// Can be specified only text or only html or both. If both, will paste html in apps that support it, elsewhere text. If only html, in apps that don't support HTML will paste html as text. /// /// Failed. Fails if there is no focused window. /// /// /// Calls and . /// To send text can use keys, characters or clipboard, depending on and text. If html not null, uses clipboard. /// /// /// /// /// Or use function and prefix "!". For HTML use prefix "%". /// bold italic", "Enter"); /// ]]> /// public static void sendt(string text, string html = null) { new keys(opt.key).AddText(text, html).SendNow(); } } //FUTURE: instead of QM2 AutoPassword: FocusPasswordField(); keys.send("!password", "Shift+Tab", "user", "Enter"); //public static void FocusPasswordField() ================================================ FILE: Au/Input/keys_types.cs ================================================ namespace Au.Types; #pragma warning disable 1591 //missing doc /// /// Modifier keys as flags. /// /// /// /// [Flags] public enum KMod : byte { Shift = 1, Ctrl = 2, Alt = 4, Win = 8, } /// /// Virtual-key codes. /// /// /// The values are the same as the native VK_ constants. Also the same as in the enum, but not as in the WPF Key enum. /// Rare and obsolete keys are not included. You can use Keys like (KKey)Keys.Attn or VK_ constant values like (KKey)200. /// /// public enum KKey : byte { MouseLeft = 0x01, MouseRight = 0x02, ///Ctrl+Pause. Break = 0x03, MouseMiddle = 0x04, MouseX1 = 0x05, MouseX2 = 0x06, Back = 0x08, Tab = 0x09, ///Shift+NumPad5, or NumPad5 when NumLock off. Clear = 0x0C, Enter = 0x0D, Shift = 0x10, Ctrl = 0x11, Alt = 0x12, Pause = 0x13, CapsLock = 0x14, IMEKanaMode = 0x15, IMEHangulMode = 0x15, IMEJunjaMode = 0x17, IMEFinalMode = 0x18, IMEHanjaMode = 0x19, IMEKanjiMode = 0x19, Escape = 0x1B, IMEConvert = 0x1C, IMENonconvert = 0x1D, IMEAccept = 0x1E, IMEModeChange = 0x1F, Space = 0x20, PageUp = 0x21, PageDown = 0x22, End = 0x23, Home = 0x24, Left = 0x25, Up = 0x26, Right = 0x27, Down = 0x28, //Select = 0x29, //Print = 0x2A, //Execute= 0x2B, PrintScreen = 0x2C, Insert = 0x2D, Delete = 0x2E, //Help = 0x2F, ///The 0 ) key. D0 = 0x30, ///The 1 ! key. D1 = 0x31, ///The 2 @ key. D2 = 0x32, ///The 3 # key. D3 = 0x33, ///The 4 $ key. D4 = 0x34, ///The 5 % key. D5 = 0x35, ///The 6 ^ key. D6 = 0x36, ///The 7 & key. D7 = 0x37, ///The 8 * key. D8 = 0x38, ///The 9 ( key. D9 = 0x39, A = 0x41, B = 0x42, C = 0x43, D = 0x44, E = 0x45, F = 0x46, G = 0x47, H = 0x48, I = 0x49, J = 0x4A, K = 0x4B, L = 0x4C, M = 0x4D, N = 0x4E, O = 0x4F, P = 0x50, Q = 0x51, R = 0x52, S = 0x53, T = 0x54, U = 0x55, V = 0x56, W = 0x57, X = 0x58, Y = 0x59, Z = 0x5A, ///The left Win key. Win = 0x5B, ///The right Win key. RWin = 0x5C, ///The Application/Menu key. Apps = 0x5D, Sleep = 0x5F, NumPad0 = 0x60, NumPad1 = 0x61, NumPad2 = 0x62, NumPad3 = 0x63, NumPad4 = 0x64, NumPad5 = 0x65, NumPad6 = 0x66, NumPad7 = 0x67, NumPad8 = 0x68, NumPad9 = 0x69, ///The numpad * key. Multiply = 0x6A, ///The numpad + key. Add = 0x6B, //Separator = 0x6C, ///The numpad - key. Subtract = 0x6D, ///The numpad . key. Decimal = 0x6E, ///The numpad / key. Divide = 0x6F, F1 = 0x70, F2 = 0x71, F3 = 0x72, F4 = 0x73, F5 = 0x74, F6 = 0x75, F7 = 0x76, F8 = 0x77, F9 = 0x78, F10 = 0x79, F11 = 0x7A, F12 = 0x7B, F13 = 0x7C, F14 = 0x7D, F15 = 0x7E, F16 = 0x7F, F17 = 0x80, F18 = 0x81, F19 = 0x82, F20 = 0x83, F21 = 0x84, F22 = 0x85, F23 = 0x86, F24 = 0x87, //VK_NAVIGATION_VIEW ... VK_NAVIGATION_CANCEL NumLock = 0x90, ScrollLock = 0x91, //VK_OEM_NEC_EQUAL ... VK_OEM_FJ_ROYA ///The left Shift key. LShift = 0xA0, ///The right Shift key. RShift = 0xA1, ///The left Ctrl key. LCtrl = 0xA2, ///The right Ctrl key. RCtrl = 0xA3, ///The left Alt key. LAlt = 0xA4, ///The right Alt key. RAlt = 0xA5, BrowserBack = 0xA6, BrowserForward = 0xA7, BrowserRefresh = 0xA8, BrowserStop = 0xA9, BrowserSearch = 0xAA, BrowserFavorites = 0xAB, BrowserHome = 0xAC, VolumeMute = 0xAD, VolumeDown = 0xAE, VolumeUp = 0xAF, MediaNextTrack = 0xB0, MediaPrevTrack = 0xB1, MediaStop = 0xB2, MediaPlayPause = 0xB3, LaunchMail = 0xB4, LaunchMediaSelect = 0xB5, LaunchApp1 = 0xB6, LaunchApp2 = 0xB7, OemSemicolon = 0xBA, OemPlus = 0xBB, OemComma = 0xBC, OemMinus = 0xBD, OemPeriod = 0xBE, OemQuestion = 0xBF, OemTilde = 0xC0, //VK_GAMEPAD_A ... VK_GAMEPAD_RIGHT_THUMBSTICK_LEFT OemOpenBrackets = 0xDB, OemPipe = 0xDC, OemCloseBrackets = 0xDD, OemQuotes = 0xDE, //VK_OEM_8 ... VK_ICO_00 IMEProcessKey = 0xE5, //VK_ICO_CLEAR ///VK_PACKET. Not a key. Packet = 0xE7, //VK_OEM_RESET ... VK_OEM_BACKTAB //Attn = 0xF6, //Crsel = 0xF7, //Exsel = 0xF8, //EraseEof = 0xF9, //Play = 0xFA, //Zoom = 0xFB, //NoName = 0xFC, //Pa1 = 0xFD, //OemClear = 0xFE, } /// /// Virtual-key code, scan code and extended-key flag for and similar functions. /// /// /// This script prints properties of pressed keys. /// { /// if(!k.IsUp) print.it(k.Key, k.vkCode, k.scanCode, k.IsExtended); /// }, ignoreAuInjected: false); /// dialog.show("Hook"); /// ]]> /// public struct KKeyScan { public KKey vk; public bool extendedKey; public ushort scanCode; public KKeyScan(KKey vk, ushort scanCode, bool extendedKey) { this.vk = vk; this.scanCode = scanCode; this.extendedKey = extendedKey; } public KKeyScan(ushort scanCode, bool extendedKey) { vk = 0; this.scanCode = scanCode; this.extendedKey = extendedKey; } public KKeyScan(KKey vk, bool extendedKey) { this.vk = vk; this.scanCode = 0; this.extendedKey = extendedKey; } } /// /// Parameter type of and similar functions. /// Has implicit conversions from string, , , , char, int (sleep time) and . /// public struct KKeysEtc { readonly object _o; KKeysEtc(object o) { _o = o; } public KKeysEtc(Action a) { _o = a; } //allows 'new(() => {})' instead of 'new Action(() => {})' public object Value => _o; public static implicit operator KKeysEtc(string s) => new(s); public static implicit operator KKeysEtc(clipboardData cd) => new(cd); public static implicit operator KKeysEtc(KKey k) => new(k); public static implicit operator KKeysEtc(KKeyScan t) => new(t); public static implicit operator KKeysEtc(char c) => new(c); public static implicit operator KKeysEtc(int ms) => new(ms); public static implicit operator KKeysEtc(Action a) => new(a); } /// /// event data. /// public class PastingEventArgs : EventArgs { /// public string Text { get; init; } /// public OKey Options { get; init; } /// public wnd WndFocus { get; init; } } #pragma warning restore 1591 /// /// Defines a hotkey as and . /// Has implicit conversion operators from string like "Ctrl+Shift+K", tuple (KMod, KKey), enum , enum Keys. /// public struct KHotkey { /// /// Modifier keys (flags). /// public KMod Mod { get; set; } /// /// Key without modifier keys. /// public KKey Key { get; set; } /// public KHotkey(KMod mod, KKey key) { Mod = mod; Key = key; } /// Implicit conversion from string like "Ctrl+Shift+K". /// Error in hotkey. public static implicit operator KHotkey(string hotkey) { if (!keys.more.parseHotkeyString(hotkey, out var mod, out var key)) throw new ArgumentException("Error in hotkey."); return new KHotkey(mod, key); } /// Implicit conversion from tuple (KMod, KKey). public static implicit operator KHotkey((KMod, KKey) hotkey) => new KHotkey(hotkey.Item1, hotkey.Item2); /// Implicit conversion from (hotkey without modifiers). public static implicit operator KHotkey(KKey key) => new KHotkey(0, key); /// Implicit conversion from like Keys.Ctrl|Keys.B. public static implicit operator KHotkey(System.Windows.Forms.Keys hotkey) => new KHotkey(keys.more.KModFromWinforms(hotkey), (KKey)(byte)hotkey); /// Explicit conversion to . public static explicit operator System.Windows.Forms.Keys(KHotkey hk) => keys.more.KModToWinforms(hk.Mod) | (System.Windows.Forms.Keys)hk.Key; /// Allows to get properties of a variable like var (mod, key) = hotkey; public void Deconstruct(out KMod mod, out KKey key) { mod = Mod; key = Key; } /// public override string ToString() => keys.more.hotkeyToString(Mod, Key); } ================================================ FILE: Au/Input/keys_util.cs ================================================ namespace Au; public partial class keys { /// /// Converts part of string to . /// The substring should contain single key name, eg "Esc", "A", "=". /// Returns 0 if invalid key name. /// static unsafe KKey _KeynameToKey(string s, int i, int len) { //print.it(s, i, len); if (len < 1) return 0; char c = s[i]; //character keys, like K, 9, - if (len == 1) { return c switch { >= 'a' and <= 'z' => (KKey)(c - 32), >= 'A' and <= 'Z' => (KKey)c, >= '0' and <= '9' => (KKey)c, '-' => KKey.OemMinus, '=' => KKey.OemPlus, '`' or '~' => KKey.OemTilde, '[' or '{' => KKey.OemOpenBrackets, ']' or '}' => KKey.OemCloseBrackets, '\\' or '|' => KKey.OemPipe, ';' or ':' => KKey.OemSemicolon, '\'' or '"' => KKey.OemQuotes, ',' or '<' => KKey.OemComma, '.' or '>' => KKey.OemPeriod, '/' or '?' => KKey.OemQuestion, _ => 0, }; //special //+ //eg Ctrl+A //* //*nTimes, *down, *up //( //eg Alt+(A F) //) //# //numpad keys, eg #5, #* //_ //character //^ //characters //reserved //! @ $ % & } //numpad keys if (c == '#') { if (len != 2) return 0; c = s[i + 1]; return c switch { >= '0' and <= '9' => (KKey)(c - '0' + (int)KKey.NumPad0), '.' => KKey.Decimal, '+' => KKey.Add, '/' => KKey.Divide, '*' => KKey.Multiply, '-' => KKey.Subtract, _ => 0, }; } //F keys if (c == 'F' && s[i + 1].IsAsciiDigit()) { int n = s.ToInt(i + 1, out int e, STIFlags.NoHex); if (n > 0 && n <= 24 && e == i + len) return (KKey)(0x6F + n); } //named keys //names start with an uppercase letter and must have at least 2 other anycase letters, except: Up, AltG (RAl), PageU (PgU), PageD (PgD), some alternative names (PU, PD, PB, PS, HM, SL, CL, NL, BS). KKey k = 0; char c1 = char.ToLowerInvariant(s[i + 1]), //note: Tables_.LowerCase would make startup slow c2 = len > 2 ? char.ToLowerInvariant(s[i + 2]) : ' ', c3 = len > 3 ? char.ToLowerInvariant(s[i + 3]) : ' ', c4 = len > 4 ? char.ToLowerInvariant(s[i + 4]) : ' '; uint u = (uint)c1 << 16 | c2; switch (c) { case 'A': if (_U('l', 't')) k = c3 == 'g' ? KKey.RAlt : KKey.Alt; else if (_U('p', 'p')) k = KKey.Apps; break; case 'B': if (_U('a', 'c') || _U('s', ' ')) k = KKey.Back; break; case 'C': if (_U('t', 'r')) k = KKey.Ctrl; else if (_U('a', 'p') || _U('l', ' ')) k = KKey.CapsLock; break; case 'D': if (_U('e', 'l')) k = KKey.Delete; else if (_U('o', 'w')) k = KKey.Down; break; case 'E': if (_U('n', 't')) k = KKey.Enter; else if (_U('n', 'd')) k = KKey.End; else if (_U('s', 'c')) k = KKey.Escape; break; case 'H': if (_U('o', 'm') || _U('m', ' ')) k = KKey.Home; break; case 'I': if (_U('n', 's')) k = KKey.Insert; break; case 'L': if (_U('e', 'f')) k = KKey.Left; //don't need LShift etc break; case 'M': if (_U('e', 'n')) k = KKey.Apps; break; case 'N': if (_U('u', 'm') || _U('l', ' ')) k = KKey.NumLock; //for NumEnter use keys.send((KKey.Enter, 0, true)) break; case 'P': if (_U('a', 'g') && c3 == 'e') k = c4 == 'u' ? KKey.PageUp : (c4 == 'd' ? KKey.PageDown : 0); else if (_U('g', 'u') || _U('u', ' ')) k = KKey.PageUp; else if (_U('g', 'd') || _U('d', ' ')) k = KKey.PageDown; else if (_U('a', 'u') || _U('b', ' ')) k = KKey.Pause; else if (_U('r', 'i') || _U('r', 't') || _U('s', ' ')) k = KKey.PrintScreen; break; case 'R': if (_U('i', 'g')) k = KKey.Right; else if (_U('a', 'l')) k = KKey.RAlt; else if (_U('c', 't')) k = KKey.RCtrl; else if (_U('s', 'h')) k = KKey.RShift; else if (_U('w', 'i')) k = KKey.RWin; break; case 'S': if (_U('h', 'i')) k = KKey.Shift; else if (_U('p', 'a')) k = KKey.Space; else if (_U('c', 'r') || _U('l', ' ')) k = KKey.ScrollLock; //SysRq not used on Windows break; case 'T': if (_U('a', 'b')) k = KKey.Tab; break; case 'U': if (c1 == 'p') k = KKey.Up; break; case 'V': if (c1 == 'k') { int v = s.ToInt(i + 2, out int end, STIFlags.DontSkipSpaces); if (end != i + len || (uint)v > 255) v = 0; return (KKey)v; } break; case 'W': if (_U('i', 'n')) k = KKey.Win; break; } if (k != 0) { for (int i2 = i + len; i < i2; i++) if (!s[i].IsAsciiAlpha()) return 0; return k; } if (c >= 'A' && c <= 'Z') { var s1 = s.Substring(i, len); #if false if(Enum.TryParse(s1, true, out KKey r1)) return r1; //if(Enum.TryParse(s1, true, out System.Windows.Forms.Keys r2) && (uint)r2 <= 0xff) return (KKey)r2; #else //20-50 times faster and less garbage. Good JIT speed. return _FindKeyInEnums(s1); #endif } return 0; bool _U(char cc1, char cc2) { return u == ((uint)cc1 << 16 | cc2); } } static IEnumerable _SplitKeysString(string keys_) => (s_rxKeys ??= new regexp(@"(?s)[A-Z][[:alnum:]]*|#\S|\*\s*(?:\d+|down|up)\b|\+\s*\(|_.|\^.+|\S")) .FindAllG(keys_ ?? "", 0); //KeyName | #n | *r | *down | *up | +( | _char | ^chars | nonspace char static regexp s_rxKeys; static System.Collections.Hashtable s_htEnum; //with Dictionary much slower JIT [MethodImpl(MethodImplOptions.NoInlining)] static KKey _FindKeyInEnums(string key) { if (s_htEnum == null) { var t = new System.Collections.Hashtable(160 /*220*/, StringComparer.OrdinalIgnoreCase); var a1 = typeof(KKey).GetFields(); for (int j = 1; j < a1.Length; j++) { //note: start from 1 to skip the default value__, it gives exception t.Add(a1[j].Name, a1[j].GetRawConstantValue()); } //rejected. Better avoid loading Forms dll. All useful keys are in KKey. For others can use virtual-key codes. //var a2 = typeof(System.Windows.Forms.Keys).GetFields(); //for(int j = 4; j < a2.Length; j++) { //skip value__, KeyCode, Modifiers, None // var v = a2[j]; // //print.it(v.Name); // if(t.ContainsKey(v.Name)) continue; // var k = v.GetRawConstantValue(); // if((uint)(int)k >= 0xff) continue; // print.it(v.Name, j); // t.Add(v.Name, k); //} //print.it(a1.Length, /*a2.Length,*/ t.Count); //216 with Keys enum, 156 without s_htEnum = t; } var r = s_htEnum[key]; if (r == null) return 0; if (r is byte) return (KKey)(byte)r; return (KKey)(int)r; //note: GetRawConstantValue gets byte for KKey, int for Keys. GetValue(null) gets of enum type. } static ArgumentException _ArgumentException_ErrorInKeysString(string keys_, int i, int len) { int end = i + len; return new ArgumentException($"Error in keys string: {keys_[..i]}■{keys_[i..end]}■{keys_[end..]}"); //tested: all fonts on Win7 have ■. } /// /// Internal static functions. /// internal static class Internal_ { /// /// Calls . /// internal static void Sleep(int ms) { if (ms > 0) wait.doEventsPrecise_(ms); //see comments in mouse._Sleep. } /// /// If t > 10, returns (t / 4 + 8). /// internal static int LimitSleepTime(int t) => t <= 10 ? t : (t / 4 + 8); /// /// If k is Shift, Ctrl, Alt or Win or their left/right versions, returns it as modifier flag, eg KMod.Shift. /// Else returns 0. /// internal static KMod KeyToMod(KKey k) { return k switch { KKey.Shift or KKey.LShift or KKey.RShift => KMod.Shift, KKey.Ctrl or KKey.LCtrl or KKey.RCtrl => KMod.Ctrl, KKey.Alt or KKey.LAlt or KKey.RAlt => KMod.Alt, KKey.Win or KKey.RWin => KMod.Win, _ => 0, }; } /// /// Gets scan code from virtual-key code. /// /// /// Keyboard layout. If 0, uses of current thread. If -1, uses current focus/active thread. internal static ushort VkToSc(KKey vk, nint hkl = 0) { if (hkl == 0) hkl = Api.GetKeyboardLayout(0); else if (hkl == -1) { var w = wnd.focused; if (w.Is0) w = wnd.active; hkl = Api.GetKeyboardLayout(w.ThreadId); } uint sc = Api.MapVirtualKeyEx((uint)vk, 0, hkl); //MAPVK_VK_TO_VSC //fix Windows bugs if (vk == KKey.PrintScreen && sc == 0x54) sc = 0x37; if (vk == KKey.Pause && sc == 0) sc = 0x45; return (ushort)sc; //tested: LCtrl, RCtrl etc are correctly sent, although MSDN does not mention that SendInput supports it. } /// /// Sends one key event. /// Just calls API SendInput with raw parameters. /// /// internal static unsafe void SendKeyEventRaw(KKey vk, ushort scan, uint flags, int? extra = null, bool dontThrow = false) { var ki = new Api.INPUTK(vk, scan, flags); if (extra != null) ki.dwExtraInfo = extra.Value; Api.SendInput(&ki, dontThrow: dontThrow); } /// internal static void SendKey(KKey k, bool? down = null, nint hkl = 0, int? extra = null, bool dontThrow = false) { uint f = 0; if (KeyTypes_.IsExtended(k)) f |= Api.KEYEVENTF_EXTENDEDKEY; ushort scan = VkToSc(k, hkl); if (down != false) SendKeyEventRaw(k, scan, f, extra, dontThrow); if (down != true) SendKeyEventRaw(k, scan, f | Api.KEYEVENTF_KEYUP, extra, dontThrow); } /// internal static void SendCtrl(bool down) => SendKeyEventRaw(KKey.Ctrl, 0x1D, down ? 0 : Api.KEYEVENTF_KEYUP); /// internal static void SendAlt(bool down) => SendKeyEventRaw(KKey.Alt, 0x38, down ? 0 : Api.KEYEVENTF_KEYUP); /// internal static void SendShift(bool down) => SendKeyEventRaw(KKey.Shift, 0x2A, down ? 0 : Api.KEYEVENTF_KEYUP); /// internal static void SendRCtrlUp() => SendKeyEventRaw(KKey.Ctrl, 0x1D, Api.KEYEVENTF_KEYUP | Api.KEYEVENTF_EXTENDEDKEY); /// internal static void SendRAltUp() => SendKeyEventRaw(KKey.Alt, 0x38, Api.KEYEVENTF_KEYUP | Api.KEYEVENTF_EXTENDEDKEY); /// internal static void SendRShiftUp() => SendKeyEventRaw(KKey.Shift, 0x36, Api.KEYEVENTF_KEYUP); /// /// Presses or releases one or more modifier keys. /// Sends in this order: Ctrl, Alt, Shift, Win. /// /// /// Modifier keys. Does nothing if 0. /// internal static unsafe void ModPressRelease(bool press, KMod mod) { if (mod == 0) return; var a = stackalloc Api.INPUTK[4]; int n = 0; uint f = press ? 0 : Api.KEYEVENTF_KEYUP; if (0 != (mod & KMod.Ctrl)) a[n++].Set(KKey.Ctrl, 0x1D, f); if (0 != (mod & KMod.Alt)) a[n++].Set(KKey.Alt, 0x38, f); if (0 != (mod & KMod.Shift)) a[n++].Set(KKey.Shift, 0x2A, f); if (0 != (mod & KMod.Win)) a[n++].Set(KKey.Win, 0x5B, f); Api.SendInput(a, n); } /// /// Releases modifier keys if pressed. /// May also press-release some other keys to avoid menu mode etc. /// Does not use options, sleep, blockinput, etc. /// /// internal static void ReleaseModAndDisableModMenu(bool dontThrow = false) { if (dontThrow) { try { ReleaseModAndDisableModMenu(); } catch (Exception ex) { Debug_.Print(ex); } return; } int m = 0; if (isPressed(KKey.LShift)) m |= 1; if (isPressed(KKey.RShift)) m |= 0x10; if (isPressed(KKey.LCtrl)) m |= 2; if (isPressed(KKey.RCtrl)) m |= 0x20; if (isPressed(KKey.LAlt)) m |= 4; if (isPressed(KKey.RAlt)) m |= 0x40; if (isPressed(KKey.Win)) m |= 8; if (isPressed(KKey.RWin)) m |= 0x80; if (m == 0) return; //if Alt or Win pressed without Ctrl, send Ctrl to avoid menu mode or Start menu. // For Alt works Ctrl-up, but maybe not everywhere. For Win need Ctrl-down-up. if (0 != (m & 0xCC) && 0 == (m & 0x22)) { SendCtrl(true); m |= 2; } //prevent invoking something when pressed-released only modifier keys. // Examples: switch keyboard layout on Ctrl+Alt or Ctrl+Shift; invoke QTranslate on two Ctrl; MS Office ad on Ctrl+Alt+Shift+Win. // The vk is unassigned. Tested: vk 0 does not work. SendKeyEventRaw((KKey)0xD8, 0, Api.KEYEVENTF_KEYUP); if (0 != (m & 2)) SendCtrl(false); if (0 != (m & 0x20)) SendRCtrlUp(); if (0 != (m & 1)) SendShift(false); if (0 != (m & 0x10)) SendRShiftUp(); if (0 != (m & 4)) SendAlt(false); if (0 != (m & 0x40)) SendRAltUp(); if (0 != (m & 8)) SendKey(KKey.Win, false); if (0 != (m & 0x80)) SendKey(KKey.RWin, false); } /// /// Sends Ctrl+V or Ctrl+C or Ctrl+X, and/or optionally one or more keys. /// Caller gets optk and wFocus with GetOptionsAndWndFocused_ (it may want to know some options too). /// Caller calls Press, waits until the target app gets clipboard data, then calls Release. /// internal unsafe struct SendCopyPaste { KHotkey _hk; ushort _scan; OKey _opt; List _andKeys; /// /// Presses Ctrl+key. Does not release. /// If andKeys used, Release will press/release them. /// /// public void Press(KHotkey hk, OKey optk, wnd wFocus, List andKeys = null) { _hk = hk; _scan = VkToSc(_hk.Key, Api.GetKeyboardLayout(wFocus.ThreadId)); _opt = optk; _andKeys = andKeys; if (_hk.Mod.Has(KMod.Ctrl)) SendCtrl(true); if (_hk.Mod.Has(KMod.Alt)) SendAlt(true); if (_hk.Mod.Has(KMod.Shift)) SendShift(true); Internal_.Sleep(_opt.KeySpeedClipboard); //eg need 100 ms for BlueStacks SendKeyEventRaw(_hk.Key, _scan, 0); } /// /// Releases keys. /// Does nothing if already released. /// /// public void Release() { if (_hk.Key == 0) return; var vk = _hk.Key; _hk.Key = 0; SendKeyEventRaw(vk, _scan, Api.KEYEVENTF_KEYUP); if (_hk.Mod.Has(KMod.Shift)) SendShift(false); if (_hk.Mod.Has(KMod.Alt)) SendAlt(false); if (_hk.Mod.Has(KMod.Ctrl)) SendCtrl(false); if (_andKeys != null) AndSendKeys(_andKeys, _opt); } /// /// Sends one or more keys. /// Not used for keys whose scancode can depend on keyboard layout. To get scancode, uses keyboard layout of current thread. /// /// public static void AndSendKeys(List keys_, OKey optk) { foreach (var k in keys_) { var f = KeyTypes_.IsExtended(k) ? _KFlags.Extended : default; var e = new _KEvent(true, k, f, VkToSc(k)); _SendKey2(e, default, true, optk); } } } /// /// Gets focused or active window. Waits for it max 20-40 ms (820 ms if requireFocus). On timeout returns default (throws exception if requireFocus). /// /// Wait for focused (and not just active) window longer, and throw exception on timeout. Used for clipboard copy/paste and send text. /// No focused window when requireFocus. /// internal static wnd GetWndFocusedOrActive(bool requireFocus) { for (int i = 0; i < (requireFocus ? 100 : 20); i++) { miscInfo.getGUIThreadInfo(out var g); //print.it(i, g.hwndFocus, g.hwndActive); if (!g.hwndFocus.Is0) return g.hwndFocus; if (!requireFocus && !g.hwndActive.Is0) return g.hwndActive; wait.ms(i < 20 ? 1 : 10); } InputDesktopException.ThrowIfBadDesktop(); if (requireFocus) throw new AuException("There is no focused window"); //TODO3: test various windows and data types, maybe somewhere could work without focus return default; //note: the purpose of this wait is not synchronization. It just makes getting the focused/active window more reliable. // Cannot wait for a focused window. Users must program it explicitly. // When creating or activating a window, often there is no focus for 200 ms or more. Eg when opening the Save As dialog. // Also, focus is optional. // Anyway, waiting for focus would not make more reliable, because keys are processed asynchronously. // Usually the active window is OK. We use it to get keyboard layout and/or to avoid calling Hook too frequently. // This func waits for active window max 20-40 ms. When switching apps, usually there is no active window for 1-5 ms. } } /// /// Returns: /// - optk - OKey of this variable or OKey cloned from this variable and possibly modified by Hook. /// - wFocus - the focused or active window. /// /// if false, the caller does not need wFocus. Then wFocus will be default(wnd) if Hook is null. /// Wait for focused (and not just active) window longer, and throw exception on timeout. Used for clipboard copy/paste and send text. /// No focused window when requireFocus. /// internal (OKey optk, wnd wFocus) GetOptionsAndWndFocused_(bool getWndAlways, bool requireFocus = false) { if (Options.Hook == null && !getWndAlways) return (Options, default); var w = Internal_.GetWndFocusedOrActive(requireFocus); return (GetOptions_(w), w); } /// /// Returns OKey of this variable or OKey cloned from this variable and possibly modified by Hook. /// /// the focused or active window. The function uses it to avoid frequent calling of Hook. If you don't have it, use GetOptionsAndWndFocused_ instead. internal OKey GetOptions_(wnd wFocus) { var call = Options.Hook; if (call == null || wFocus.Is0) return Options; if (wFocus != _sstate.wFocus) { _sstate.wFocus = wFocus; if (_sstate.options == null) _sstate.options = new OKey(Options); else _sstate.options.CopyOrDefault_(Options); call(new OKeyHookData(_sstate.options, wFocus)); } return _sstate.options; } void _ThrowIfSending() { if (_sending) throw new InvalidOperationException(); } internal static class KeyTypes_ { [Flags] enum _KT : byte { Mod = 1, Extended = 2, Mouse = 4, GksReliable = 8, } /// /// Ctrl, LCtrl, etc. /// public static bool IsMod(KKey vk) => 0 != (_b[(byte)vk] & _KT.Mod); public static bool IsExtended(KKey vk) => 0 != (_b[(byte)vk] & _KT.Extended); public static bool IsMouse(KKey vk) => 0 != (_b[(byte)vk] & _KT.Mouse); /// /// API GetKeyState always works. /// For other keys returns 0 if pressed or toggled before starting current thread. /// Modifiers (left/right too), lock keys, mouse, some other. /// public static bool IsGetKeyStateReliable(KKey vk) => 0 != (_b[(byte)vk] & _KT.GksReliable); /// /// The same as . /// public static bool IsToggleable(KKey vk) => 0 != (_b[(byte)vk] & _KT.GksReliable); static _KT[] _b; static KeyTypes_() { _b = new _KT[256]; _b[1] = _b[2] = _b[4] = _b[5] = _b[6] = _KT.Mouse | _KT.GksReliable; _b[16] = _b[17] = _b[18] = _b[(int)KKey.LShift] = _b[(int)KKey.RShift] = _b[(int)KKey.LCtrl] = _b[(int)KKey.LAlt] = _KT.Mod | _KT.GksReliable; _b[(int)KKey.PageUp] = _b[(int)KKey.PageDown] = _b[(int)KKey.End] = _b[(int)KKey.Home] = _b[(int)KKey.Left] = _b[(int)KKey.Up] = _b[(int)KKey.Right] = _b[(int)KKey.Down] = _b[(int)KKey.PrintScreen] = _b[(int)KKey.Insert] = _b[(int)KKey.Delete] = _b[(int)KKey.Sleep] = _b[(int)KKey.Apps] = _b[(int)KKey.Divide] = _b[(int)KKey.Break] = _KT.Extended; //and more, but undocumented, and cannot test. There is no API to get extended keys. MapVirtualKeyEx can get only of 50% keys. _b[(int)KKey.CapsLock] = _b[(int)KKey.ScrollLock] = _b[(int)KKey.Back] = _b[(int)KKey.Tab] = _b[(int)KKey.Enter] = _b[(int)KKey.Escape] = _KT.GksReliable; //also Home and maybe more _b[(int)KKey.NumLock] = _KT.Extended | _KT.GksReliable; _b[(int)KKey.Win] = _b[(int)KKey.RWin] = _b[(int)KKey.RCtrl] = _b[(int)KKey.RAlt] = _KT.Mod | _KT.Extended | _KT.GksReliable; for (int i = (int)KKey.BrowserBack; i <= (int)KKey.LaunchApp2; i++) _b[i] = _KT.Extended; //media/browser/launchapp keys //for(int i = 1; i < 256; i++) print.it((KKey)i, _b[i]); } } struct _INPUTKEY2 { public Api.INPUTK k0, k1; public _INPUTKEY2(KKey vk, ushort sc, uint flags = 0) { k0 = new Api.INPUTK(vk, sc, flags); k1 = new Api.INPUTK(vk, sc, flags | Api.KEYEVENTF_KEYUP); } } } ================================================ FILE: Au/Input/miscInfo.cs ================================================ namespace Au; /// /// Contains functions to get miscellaneous info not found in other classes of this library and .NET. /// /// /// /// /// /// /// /// /// /// /// /// public static class miscInfo { /// /// Calls API GetGUIThreadInfo. It gets info about mouse capturing, menu mode, move/size mode, focus, caret, etc. /// /// API GUITHREADINFO. /// Thread id. If 0 - the foreground (active window) thread. See , . public static unsafe bool getGUIThreadInfo(out GUITHREADINFO g, int idThread = 0) { g = new GUITHREADINFO { cbSize = sizeof(GUITHREADINFO) }; return Api.GetGUIThreadInfo(idThread, ref g); } /// /// Gets caret rectangle. /// /// false if failed. /// Receives the rectangle, in screen coordinates. /// Receives the caret owner control or the focused control. /// If fails, get mouse pointer coordinates. /// /// Some apps use non-standard caret; then may fail. /// public static bool getTextCursorRect(out RECT r, out wnd w, bool orMouse = false) { //rejected. Too few controls support it. ///// Get text selection rectangle if possible. if (getGUIThreadInfo(out var g)) { if (!g.hwndCaret.Is0) { if (g.rcCaret.bottom <= g.rcCaret.top) g.rcCaret.bottom = g.rcCaret.top + 16; r = g.rcCaret; g.hwndCaret.MapClientToScreen(ref r); w = g.hwndCaret; return true; } if (!g.hwndFocus.Is0) { w = g.hwndFocus; try { var e = elm.fromWindow(g.hwndFocus, EObjid.CARET, EWFlags.NoThrow | EWFlags.NotInProc); if (e?.GetRect(out r) == true) return true; if (g.hwndFocus.ClassNameIs("HwndWrapper[powershell_ise.exe;*")) { if (UiaUtil.GetCaretRectInPowerShell(out r)) return true; } else if (UiaUtil.ElementFocused() is { } ef) { if (ef.GetCaretRect(out r)) return true; } //GetGUIThreadInfo and MSAA don't work with winstore, winui3, Windows Terminal. //Most winstore and winui3 apps support IUIAutomationTextPattern2, and it gives correct caret rect. //Terminal supports only IUIAutomationTextPattern, and it gives correct caret rect when there is no selection. //Bad: some apps give client coordinates (bug, eg PowerShell). We can convert to screen easily, but can't know the coordinate type in unknown apps. //Win+; works well with all tested winstore apps and terminal, although some apps don't support even IUIAutomationTextPattern. What API it uses? //IME works everywhere. What API it uses? //PhraseExpress doesn't work. } catch (Exception e1) { Debug_.Print(e1); } } } if (orMouse) { Api.GetCursorPos(out var p); r = new RECT(p.x, p.y, 0, 16); } else r = default; w = default; return false; //note: in Word, after changing caret pos, GetGUIThreadInfo and MSAA get pos 0 0. After 0.5 s gets correct. After typing always correct. } /// /// Returns true if current thread is on the input desktop and therefore can use mouse, keyboard, clipboard and window functions. /// /// Return false if the active window is a full-screen window of LockApp.exe on Windows 10+. For example when computer has been locked but still not displaying the password field. Slower. /// /// Usually this app is running on default desktop. Examples of other desktops: the Ctrl+Alt+Delete screen, the PC locked screen, screen saver, UAC consent, custom desktops. If one of these is active, this process cannot use many mouse, keyboard, clipboard and window functions. They either throw exception or do nothing. /// /// public static unsafe bool isInputDesktop(bool detectLocked = false) { var w = wnd.active; if (w.Is0) { //tested: last error code 0 int i = 0; if (!Api.GetUserObjectInformation(Api.GetThreadDesktop(Api.GetCurrentThreadId()), Api.UOI_IO, &i, 4, out _)) return true; //slow return i != 0; //also tested several default screensavers on Win10 and 7. Goes through this branch. When closed, works like when locked (goes through other branch until next input). } else { if (detectLocked && osVersion.minWin10) { var rw = w.Rect; if (rw.left == 0 && rw.top == 0) { var rs = screen.primary.Rect; if (rw == rs) { var s = w.ProgramName; if (s.Eqi("LockApp.exe")) return false; } } } return true; //info: in lock screen SHQueryUserNotificationState returns QUNS_NOT_PRESENT. // Also documented but not tested: screen saver. // tested: QUNS_ACCEPTS_NOTIFICATIONS (normal) when Ctrl+Alt+Delete. // However it is too slow, eg 1300 mcs. } } //public static unsafe string GetInputDesktopName() { // var hd = Api.OpenInputDesktop(0, false, Api.GENERIC_READ); //error "Access is denied" when this process is admin. Need SYSTEM. // //if (hd == default) throw new AuException(0); // if (hd == default) return null; // string s = null; // var p = stackalloc char[300]; // if (Api.GetUserObjectInformation(hd, Api.UOI_NAME, p, 600, out int len) && len >= 4) s = new(p, 0, len / 2 - 1); // Api.CloseDesktop(hd); // return s; //} /// /// Returns true if this process is running in a child session (aka Picture-in-Picture). /// public static unsafe bool isChildSession { get { if (!s_isChildSession.HasValue) { bool yes = false; if (osVersion.minWin8) { var psid = process.thisProcessSessionId; if (psid > 1) { int csid = Api.WTSGetActiveConsoleSessionId(); if (csid != -1 && psid != csid) { if (Api.WTSQuerySessionInformation(-1, Api.WTS_INFO_CLASS.WTSIsRemoteSession, out bool isRemote) && isRemote) { if (Api.WTSQuerySessionInformation(-1, Api.WTS_INFO_CLASS.WTSWinStationName, out string s1)) { yes = !s1.Starts("RDP-"); } } } } } s_isChildSession = yes; } return s_isChildSession.Value; } } static bool? s_isChildSession; } ================================================ FILE: Au/Input/mouse.cs ================================================ //TODO3: test how mouse moves through non-screen area between screens A and C when screen B is in between. // QM2 has problems crossing non-screen corners at default speed. Au works well. namespace Au; /// /// Mouse functions. /// /// /// Should not be used to click windows of own thread. It may work or not. If need, use another thread. Example in . /// public static class mouse { /// /// Gets cursor (mouse pointer) position. /// /// /// /// /// public static POINT xy { get { Api.GetCursorPos(out var p); return p; } } static void _Move(POINT p, bool fast) { bool relaxed = opt.mouse.Relaxed, willFail = false; if (!screen.isInAnyScreen(p)) { if (!relaxed) throw new ArgumentOutOfRangeException(null, "Cannot mouse-move. This x y is not in screen. " + p.ToString()); willFail = true; } if (!fast) _MoveSlowTo(p); POINT p0 = xy; //bool retry = false; g1: bool ok = false; for (int i = 0, n = relaxed ? 3 : 10; i < n; i++) { //perf.first(); _SendMove(p); //info: now xy is still not updated in ~10% cases. // In my tests was always updated after sleeping 0 or 1 ms. // But the user etc also can move the mouse at the same time. Then the i loop always helps. //perf.next(); int j = 0; for (; ; j++) { var pNow = xy; ok = (pNow == p); if (ok || pNow != p0 || j > 3) break; wait.ms(j); //0+1+2+3 } //perf.nw(); //print.it(j, i); if (ok || willFail) break; //note: don't put the _Sleep(7) here //Accidentally this is also a workaround for SendInput bug: // When moving to other screen, moves to a wrong place if new x or y is outside of rect of old screen. // To reproduce: // Let now mouse is in second screen, and try to move to 0 0 (primary screen). // Case 1: Let second screen is at the bottom and its left is eg 300. Single SendInput will move to 300 0. // Case 2: Let second screen is at the right and its top is eg 300. Will move x to the right of the primary screen. // Tested only on Win10. // Workaround: call SendInput twice. } if (!ok && !relaxed) { var es = $"*mouse-move to this x y in screen. " + p.ToString(); wnd.active.UacCheckAndThrow_(es + ". The active"); //it's a mystery for users. API SendInput fails even if the point is not in the window. //rejected: wnd.getwnd.root.ActivateL() InputDesktopException.ThrowIfBadDesktop(es); throw new AuException(es); //known reasons: // Active window of higher UAC IL. // BlockInput, hook, some script etc that blocks mouse movement or restores mouse position. // ClipCursor. } s_prevMousePos.last = p; _Sleep(opt.mouse.MoveSleepFinally); } static void _MoveSlowTo(POINT p) { bool drag = t_pressedButtons != 0; int speed = opt.mouse.MoveSpeed; if (drag) speed++; else if (speed == 0) return; //need at least 1 intermediate point, else some apps don't drag or don't select text etc var p1 = mouse.xy; //the start point; p is the end point int x2 = p.x - p1.x, y2 = p.y - p1.y; //x and y distances bool xNeg, yNeg; if (xNeg = x2 < 0) x2 = -x2; if (yNeg = y2 < 0) y2 = -y2; //make code easier double dist = Math.Sqrt(x2 * x2 + y2 * y2); if (dist < 4) return; double angle = Math.Atan2(y2, x2); var (sin, cos) = Math.SinCos(angle); double speed2 = speed / 10.0; for (double z = 0; ;) { bool startDragSlowly = drag && z < Math.Min(20, dist); //some apps refuse to drag if too fast double d = ((startDragSlowly ? z : dist - z) / 10 + 1) / speed2; //the speed depends on the distance, and is decreasing; increasing while startDragSlowly if (d > dist / 2) d = dist / 2; else if (d < 2) d = 2; //need at least 1 intermediate point; don't need too many points z += d; int x = (z * cos).ToInt(), y = (z * sin).ToInt(); //make the line smoother. Converting double to int creates ugly bumps etc. // Usually don't need it, but in some cases it may be better. // Try to add 1 to x or/and y and use it if then the angle difference is smaller. int xPlus = 0, yPlus = 0; double anDiff = 4; for (int i = 0; i < 2; i++) { for (int j = 0; j < 2; j++) { double ad = Math.Abs(Math.Atan2(y + j, x + i) - angle); if (ad < anDiff) { anDiff = ad; xPlus = i; yPlus = j; } } } if (xPlus > 0 || yPlus > 0) { x += xPlus; y += yPlus; z = Math.Sqrt(x * x + y * y); } if (dist - z < .5) break; _SendMove(new(p1.x + (xNeg ? -x : x), p1.y + (yNeg ? -y : y))); _Sleep(7 + (speed - 1) / 10); //7-8 is the natural max WM_MOUSEMOVE period, even when the system timer period is 15.625 (default). } } /// /// Moves the cursor (mouse pointer) to the position x y relative to window w. /// /// Cursor position in screen coordinates. /// Window or control. /// X coordinate relative to the client area of w. Default - center. Examples: 10, ^10 (reverse), .5f (fraction). /// Y coordinate relative to the client area of w. Default - center. /// x y are relative to the window rectangle. /// /// - Invalid window. /// - The window is hidden. No exception if just cloaked, for example in another desktop; then on click will activate, which usually uncloaks. No exception if w is a control. /// - Other window-related failures. /// /// public static POINT move(wnd w, Coord x = default, Coord y = default, bool nonClient = false) { WaitForNoButtonsPressed_(); w.ThrowIfInvalid(); var wTL = w.Window; if (!wTL.IsVisible) throw new AuWndException(wTL, "Cannot mouse-move. The window is invisible"); //should make visible? Probably not. If cloaked because in an inactive virtual desktop etc, Click activates and it usually uncloaks. if (wTL.IsMinimized) { wTL.ShowNotMinimized(1); _Sleep(500); } //never mind: if w is a control... var p = Coord.NormalizeInWindow(x, y, w, nonClient, centerIfEmpty: true); if (!w.MapClientToScreen(ref p)) w.ThrowUseNative(); _Move(p, fast: false); return p; } /// /// Moves the cursor (mouse pointer) to the position x y relative to UI object obj. /// /// Can be , , , , in screen, in window, (true), (false) /// X coordinate relative to obj. Default - center. Examples: 10, ^10 (reverse), .5f (fraction). /// Y coordinate relative to obj. Default - center. /// /// Other exceptions. Depends on obj type. public static void move(MObject obj, Coord x = default, Coord y = default) { switch (obj.Value) { case wnd w: move(w, x, y); break; case elm e: e.MouseMove(x, y); break; case uiimage u: u.MouseMove(x, y); break; case RECT r: move(Coord.NormalizeInRect(x, y, r, centerIfEmpty: true)); break; case (wnd w, RECT r): var p = Coord.NormalizeInRect(x, y, r, centerIfEmpty: true); move(w, p.x, p.y); break; case (wnd w, bool nonClient): move(w, x, y, nonClient); break; case (screen s, bool workArea): move(Coord.Normalize(x, y, workArea, s, centerIfEmpty: true)); break; case bool useLastXY: move(_CoordToRelativeXY(x, y, useLastXY)); break; case null: //default(MObject) move(x, y); break; } } static POINT _CoordToRelativeXY(Coord x, Coord y, bool useLastXY) { var p = useLastXY ? lastXY : xy; if (x.Type is CoordType.Normal or CoordType.None) p.x += x.Value; else throw new ArgumentException(null, "x"); if (y.Type is CoordType.Normal or CoordType.None) p.y += y.Value; else throw new ArgumentException(null, "y"); return p; } /// /// Moves the cursor (mouse pointer) to the specified position in screen. /// /// Normalized cursor position. /// X coordinate. Examples: 10, ^10 (reverse), .5f (fraction). /// Y coordinate. /// public static POINT move(Coord x, Coord y) { WaitForNoButtonsPressed_(); var p = Coord.Normalize(x, y); _Move(p, fast: false); return p; } //rejected: parameters bool workArea = false, screen screen = default. Rarely used. Can use the POINT overload and Coord.Normalize. /// /// Moves the cursor (mouse pointer) to the specified position in screen. /// /// /// Coordinates. /// Tip: To specify coordinates relative to the right, bottom, work area or a non-primary screen, use , like in the example. /// /// The position is not in screen. No exception if option Relaxed is true (then moves to a screen edge). /// /// Failed to move the cursor to that position. Some reasons: /// - The active window belongs to a process of higher [](xref:uac) integrity level. /// - Another thread blocks or modifies mouse input (API BlockInput, mouse hooks, frequent API SendInput etc). /// - Some application called API ClipCursor. No exception if option Relaxed is true (then final cursor position is undefined). /// /// /// /// Uses : , , . /// /// /// Save-restore mouse position. /// /// Use coordinates in the first non-primary screen. /// /// public static void move(POINT p) { WaitForNoButtonsPressed_(); _Move(p, fast: false); } /// /// Remembers current mouse cursor position to be later restored with . /// public static void save() { if (s_prevMousePos is { } v) v.first = xy; else s_prevMousePos = new(); } /// /// Moves the mouse cursor where it was at the time of the last . If it was not called - of the first "mouse move" or "mouse click" function call. Does nothing if these functions were not called. /// /// /// Uses : , . /// /// public static void restore() { if (s_prevMousePos is { first: var p }) { WaitForNoButtonsPressed_(); _Move(p, fast: true); } } class _PrevMousePos { public POINT first, last; public _PrevMousePos() { first = last = xy; } public _PrevMousePos(POINT p) { first = last = p; } } static _PrevMousePos s_prevMousePos; /// /// Mouse cursor position of the most recent successful "mouse move" or "mouse click" function call. /// If such functions are still not called, returns . /// public static POINT lastXY => s_prevMousePos?.last ?? xy; //rejected. MoveRelative usually is better. If need, can use code: Move(mouse.xy.x+dx, mouse.xy.y+dy). //public static void MoveFromCurrent(int dx, int dy) //{ // var p = XY; // p.Offset(dx, dy); // Move(p); //} /// /// Moves the cursor (mouse pointer) relative to or . /// /// Final cursor position in screen. /// X offset from lastXY.x or xy.x. /// Y offset from lastXY.y or xy.y. /// If true (default), moves relative to , else relative to . /// public static POINT moveBy(int dx, int dy, bool useLastXY = true) { WaitForNoButtonsPressed_(); var p = useLastXY ? lastXY : xy; p.x += dx; p.y += dy; _Move(p, fast: false); return p; } /// /// Moves the cursor (mouse pointer) relative to . Uses multiple x y offsets. /// /// Final cursor position in screen. /// String containing multiple x y offsets. Created by a mouse recorder tool with . /// Speed factor. For example, 0.5 makes 2 times faster. /// Invalid Base64 string. /// The string is not compatible with this library version (recorded with a newer version and has additional options). /// /// /// Uses : (only for the last movement; always relaxed in intermediate movements). /// public static POINT moveBy(string offsets, double speedFactor = 1.0) { WaitForNoButtonsPressed_(); var a = Convert.FromBase64String(offsets); byte flags = a[0]; const int knownFlags = 1; if ((flags & knownFlags) != flags) throw new ArgumentException("Unknown string version"); bool withSleepTimes = 0 != (flags & 1); bool isSleep = withSleepTimes; var p = lastXY; int pdx = 0, pdy = 0; for (int i = 1; i < a.Length;) { if (i > 1 && (isSleep || !withSleepTimes)) { _SendMove(p); if (!withSleepTimes) _Sleep((7 * speedFactor).ToInt()); } int v = a[i++], nbytes = (v & 3) + 1; for (int j = 1; j < nbytes; j++) v |= a[i++] << j * 8; v = (int)((uint)v >> 2); if (isSleep) { //print.it($"nbytes={nbytes} sleep={v}"); _Sleep((v * speedFactor).ToInt()); } else { int shift = nbytes * 4 - 1, mask = (1 << shift) - 1; int x = v & mask, y = (v >> shift) & mask; shift = 32 - shift; x <<= shift; x >>= shift; y <<= shift; y >>= shift; //sign-extend int dx = pdx + x; pdx = dx; int dy = pdy + y; pdy = dy; //print.it($"dx={dx} dy={dy} x={x} y={y} nbytes={nbytes} v=0x{v:X}"); p.x += dx; p.y += dy; } isSleep ^= withSleepTimes; } _Move(p, fast: true); return p; } /// /// Sends single mouse movement event. /// x y are normal absolute coordinates. /// static void _SendMove(POINT p) { s_prevMousePos ??= new(); //sets .first=.last=mouse.xy _SendRaw(Api.IMFlags.Move, p.x, p.y); } /// /// Sends single mouse button down or up event. /// Does not use the action flags of button. /// Applies SM_SWAPBUTTON. /// Also moves to p in the same API SendInput call. /// internal static void SendButton_(MButton button, bool down, POINT p) { //CONSIDER: release user-pressed modifier keys, like keys class does. //CONSIDER: block user input, like keys class does. Api.IMFlags f; MButtons mb; switch (button & (MButton.Left | MButton.Right | MButton.Middle | MButton.X1 | MButton.X2)) { case 0: //allow 0 for left. Example: wnd.find(...).MouseClick(x, y, MButton.DoubleClick) case MButton.Left: f = down ? Api.IMFlags.LeftDown : Api.IMFlags.LeftUp; mb = MButtons.Left; break; case MButton.Right: f = down ? Api.IMFlags.RightDown : Api.IMFlags.RightUp; mb = MButtons.Right; break; case MButton.Middle: f = down ? Api.IMFlags.MiddleDown : Api.IMFlags.MiddleUp; mb = MButtons.Middle; break; case MButton.X1: f = down ? Api.IMFlags.XDown | Api.IMFlags.X1 : Api.IMFlags.XUp | Api.IMFlags.X1; mb = MButtons.X1; break; case MButton.X2: f = down ? Api.IMFlags.XDown | Api.IMFlags.X2 : Api.IMFlags.XUp | Api.IMFlags.X2; mb = MButtons.X2; break; default: throw new ArgumentException("Several buttons specified", nameof(button)); //rejected: InvalidEnumArgumentException. It's in System.ComponentModel namespace. } //maybe mouse left/right buttons are swapped if (0 != (button & (MButton.Left | MButton.Right)) && 0 != Api.GetSystemMetrics(Api.SM_SWAPBUTTON)) f ^= down ? Api.IMFlags.LeftDown | Api.IMFlags.RightDown : Api.IMFlags.LeftUp | Api.IMFlags.RightUp; //If this is a Click(x y), the sequence of sent events is like: move, sleep, down, sleep, up. Even Click() sleeps between down and up. //During the sleep the user can move the mouse. Correct it now if need. //tested: if don't need to move, mouse messages are not sent. Hooks not tested. In some cases are sent one or more mouse messages but it depends on other things. //Alternatively could temporarily block user input, but it is not good. Need a hook (UAC disables Api.BlockInput), etc. Better let scripts do it explicitly. If script contains several mouse/keys statements, it's better to block input once for all. f |= Api.IMFlags.Move; //normally don't need this, but this is a workaround for the SendInput bug with multiple screens if (p != xy) _SendRaw(Api.IMFlags.Move, p.x, p.y); _SendRaw(f, p.x, p.y); if (down) t_pressedButtons |= mb; else t_pressedButtons &= ~mb; } /// /// Calls Api.SendInput to send single mouse movement or/and button down or up or wheel event. /// Converts x, y as need for MOUSEINPUT. /// For X buttons use Api.IMFlag.XDown|Api.IMFlag.X1 etc. /// If Api.IMFlag.Move, adds Api.IMFlag.Absolute. /// static unsafe void _SendRaw(Api.IMFlags flags, int x = 0, int y = 0, int wheel = 0) { if (0 != (flags & Api.IMFlags.Move)) { flags |= Api.IMFlags.Absolute; var psr = screen.primary.Rect; x = (int)((((long)x << 16) + (x >= 0 ? 0x8000 : -0x8000)) / psr.Width); y = (int)((((long)y << 16) + (y >= 0 ? 0x8000 : -0x8000)) / psr.Height); } int mouseData; if (0 != (flags & (Api.IMFlags.XDown | Api.IMFlags.XUp))) { mouseData = (int)((uint)flags >> 24); flags &= (Api.IMFlags)0xffffff; } else mouseData = wheel; var k = new Api.INPUTM(flags, x, y, mouseData); Api.SendInput(&k); } static void _Sleep(int ms) { wait.doEventsPrecise_(ms); //note: always doevents, even if window from point is not of our thread. Because: // Cannot always reliably detect what window will receive the message and what then happens. // There is not much sense to avoid doevents. If no message loop, it is fast and safe; else the script author should use another thread or expect anything. // API SendInput dispatches sent messages anyway. // _Click shows warning if window of this thread. //FUTURE: sync better, especially finally. } [ThreadStatic] static MButtons t_pressedButtons; static void _Click(MButton button, POINT p, wnd w = default) { if (w.Is0) w = Api.WindowFromPoint(p); bool windowOfThisThread = w.IsOfThisThread; if (windowOfThisThread) print.warning("Click(window of own thread) may not work. Use another thread."); //Sending a click to a window of own thread often does not work. //Reason 1: often the window on down event enters a message loop that waits for up event. But then this func cannot send the up event because it is in the loop (if it does doevents). // Known workarounds: // 1 (applied). Don't sleepdoevents between sending down and up events. // 2. Let this func send the click from another thread, and sleepdoevents until that thread finishes the click. //Reason 2: if this func called from a click handler, OS does not send more mouse events. // Known workarounds: // 1. (applied): show warning. Let the user modify the script: either don't click own windows or click from another thread. int sleep = opt.mouse.ClickSpeed; switch (button & (MButton.Down | MButton.Up | MButton.DoubleClick)) { case MButton.DoubleClick: sleep = Math.Min(sleep, Api.GetDoubleClickTime() / 4); //info: default double-click time is 500. Control Panel can set 200-900. API can set 1. //info: to detect double-click, some apps use time between down and down (that is why /4), others between up and down. SendButton_(button, true, p); if (!windowOfThisThread) _Sleep(sleep); SendButton_(button, false, p); if (!windowOfThisThread) _Sleep(sleep); goto case 0; case 0: //click SendButton_(button, true, p); if (!windowOfThisThread) _Sleep(sleep); SendButton_(button, false, p); break; case MButton.Down: SendButton_(button, true, p); break; case MButton.Up: SendButton_(button, false, p); break; default: throw new ArgumentException("Incompatible flags: Down, Up, DoubleClick", nameof(button)); } _Sleep(sleep + opt.mouse.ClickSleepFinally); //rejected: detect click failures (UAC, BlockInput, hooks). // Difficult. Cannot detect reliably. SendInput returns true. // Eg when blocked by UAC, GetKeyState shows changed toggle state. Then probably hooks also called, did not test. } /// /// Clicks, double-clicks, presses or releases a mouse button at position x y relative to window w. /// /// The return value can be used to auto-release the pressed button. Example: . /// Button and action. Default: left click. /// Invalid button flags (multiple buttons or actions specified). /// /// /// /// public static MRelease clickEx(MButton button, wnd w, Coord x = default, Coord y = default, bool nonClient = false) { POINT p = move(w, x, y, nonClient); //Make sure will click w, not another window. var action = button & (MButton.Down | MButton.Up | MButton.DoubleClick); if (action != MButton.Up && !opt.mouse.Relaxed) { //allow to release anywhere, eg it could be a drag-drop var wTL = w.Window; bool bad = !wTL.Rect.Contains(p); if (!bad) { if (!_CheckWindowFromPoint()) { //Debug_.Print("need to activate"); //info: activating brings to the Z top and also uncloaks if (!wTL.IsEnabled(false)) bad = true; //probably an owned modal dialog disabled the window else if (wTL.ThreadId == wnd.getwnd.shellWindow.ThreadId) bad = true; //desktop else if (wTL.IsActive) wTL.ZorderTop(); //can be below another window in the same topmost/normal Z order, although it is rare. else bad = !wTL.Activate_(wnd.Internal_.ActivateFlags.NoThrowIfInvalid | wnd.Internal_.ActivateFlags.IgnoreIfNoActivateStyleEtc | wnd.Internal_.ActivateFlags.NoGetWindow); //rejected: if wTL is desktop, minimize windows. Scripts should not have a reason to click desktop. If need, they can minimize windows explicitly. //CONSIDER: activate always, because some controls don't respond when clicked while the window is inactive. But there is a risk to activate a window that does not want to be activated on click, even if we don't activate windows that have noactivate style. Probably better let the script author insert Activate before Click when need. //CONSIDER: what if the window is hung? if (!bad) bad = !_CheckWindowFromPoint(); } else if (!wTL.IsActive && !wTL.IsNoActivateStyle_()) { //activate window, because some windows/controls have this nasty feature: // If window inactive, the first click just activates the window but does not execute the click action. // Example: ribbon controls. // Usually on WM_MOUSEACTIVATE they return MA_ACTIVATEANDEAT. We could send the message to detect it, but it's dirty and dangerous, eg some windows try to activate or focus self. // In any case, activating could make more reliable. In QM2 it worked well, don't remember any problems. wTL.ActivateL(); wTL.MinimalSleepIfOtherThread_(); } } if (bad) throw new AuWndException(wTL, "Cannot click. The point is not in the window"); bool _CheckWindowFromPoint() { var wfp = wnd.fromXY(p, WXYFlags.NeedWindow); if (wfp == wTL) return true; //forgive if same thread and no title bar. Eg a tooltip that disappears and relays the click to its owner window. But not if wTL is disabled. if (wTL.IsEnabled(false) && wfp.ThreadId == wTL.ThreadId && !wfp.HasStyle(WS.CAPTION)) return true; return false; } } _Click(button, p, w); return button; } /// /// Clicks, double-clicks, presses or releases a mouse button at position x y relative to UI object obj. /// /// Button and action. Default: left click. /// Can be , (), (), , in screen, in window, (true), (false). /// X coordinate relative to obj. Default - center. Examples: 10, ^10 (reverse), .5f (fraction). /// Y coordinate relative to obj. Default - center. /// /// /// Other exceptions. Depends on obj type. public static MRelease clickEx(MButton button, MObject obj, Coord x = default, Coord y = default) { switch (obj.Value) { case wnd w: return clickEx(button, w, x, y); case elm e: return e.MouseClick(x, y, button); case uiimage u: return u.MouseClick(x, y, button); case RECT r: return mouse.clickEx(button, Coord.NormalizeInRect(x, y, r, centerIfEmpty: true)); case (wnd w, RECT r): var p = Coord.NormalizeInRect(x, y, r, centerIfEmpty: true); return mouse.clickEx(button, w, p.x, p.y); case (wnd w, bool nonClient): return mouse.clickEx(button, w, x, y, nonClient); case (screen s, bool workArea): return mouse.clickEx(button, Coord.Normalize(x, y, workArea, s, centerIfEmpty: true)); case bool useLastXY: return mouse.clickEx(button, _CoordToRelativeXY(x, y, useLastXY)); case null: //default(MObject) return clickEx(button, x, y); } return default; //never } /// /// Clicks, double-clicks, presses or releases a mouse button at the specified position in screen. /// /// The return value can be used to auto-release the pressed button. Example: . /// Button and action. Default: left click. /// Invalid button flags (multiple buttons or actions specified). /// public static MRelease clickEx(MButton button, Coord x, Coord y) { POINT p = move(x, y); _Click(button, p); return button; } /// /// Coordinates. /// Tip: To specify coordinates relative to the right, bottom, work area or a non-primary screen, use , like in the example. /// /// /// /// Click at 100 200. /// /// /// Right-click at 50 from left and 100 from bottom of the work area. /// /// public static MRelease clickEx(MButton button, POINT p) { move(p); _Click(button, p); return button; } /// /// Clicks, double-clicks, presses or releases a mouse button. /// By default does not move the mouse cursor. /// /// The return value can be used to auto-release the pressed button. Example: . /// Button and action. Default: left click. /// /// Use . It is the mouse cursor position set by the most recent "mouse move" or "mouse click" function. Use this option for reliability. /// Example: mouse.move(100, 100); mouse.clickEx(..., true);. The click is always at 100 100, even if somebody changes cursor position between mouse.move sets it and mouse.clickEx uses it. In such case this option atomically moves the cursor to . This movement is instant and does not use . /// If false (default), clicks at the current cursor position (does not move it). /// /// Invalid button flags (multiple buttons or actions specified). /// If lastXY true and need to move the cursor - exceptions of . /// /// /// Uses : , and maybe those used by . /// public static MRelease clickEx(MButton button = MButton.Left, bool useLastXY = false) { POINT p; if (useLastXY) p = lastXY; else { p = xy; if (s_prevMousePos is { } v) v.last = p; else s_prevMousePos = new(p); //sets .first=.last=p } _Click(button, p); return button; } /// /// Left button click at position x y relative to window w. /// /// Window or control. /// X coordinate relative to the client area of w. Default - center. Examples: 10, ^10 (reverse), .5f (fraction). /// Y coordinate relative to the client area of w. Default - center. /// The specified position is relative to the window rectangle, not to its client area. /// /// - The specified position is not in the window (read more in Remarks). /// - Invalid window. /// - The window is hidden. No exception if just cloaked, for example in another desktop; then on click will activate, which usually uncloaks. No exception if w is a control. /// - Other window-related failures. /// /// /// /// To move the mouse cursor, calls . /// If after moving the cursor it is not in the window (or a window of its thread), activates the window (or its top-level parent window). Throws exception if then x y is still not in the window. Skips all this when just releasing button or if option Relaxed is true. If w is a control, x y can be somewhere else in its top-level parent window. /// /// Uses : , (between moving and clicking), , , . /// public static void click(wnd w, Coord x = default, Coord y = default, bool nonClient = false) { clickEx(MButton.Left, w, x, y, nonClient); } /// /// Left button click at position x y. /// /// X coordinate in the screen. Examples: 10, ^10 (reverse), .5f (fraction). /// Y coordinate in the screen. /// /// /// Uses : , and those used by . /// public static void click(Coord x, Coord y) { //note: most Click functions don't have a workArea and screen parameter. It is rarely used. For reliability better use the overloads that use window coordinates. clickEx(MButton.Left, x, y); } /// /// Left button click. /// /// Use , not current cursor position. More info: . /// If lastXY true and need to move the cursor - exceptions of . /// /// /// Uses : , and maybe those used by . /// public static void click(bool useLastXY = false) { clickEx(MButton.Left, useLastXY); } /// /// Right button click at position x y relative to window w. /// /// public static void rightClick(wnd w, Coord x = default, Coord y = default, bool nonClient = false) { clickEx(MButton.Right, w, x, y, nonClient); } /// /// Right button click at position x y. /// /// public static void rightClick(Coord x, Coord y) { clickEx(MButton.Right, x, y); } /// /// Right button click. /// /// public static void rightClick(bool useLastXY = false) { clickEx(MButton.Right, useLastXY); } /// /// Left button double click at position x y relative to window w. /// /// public static void doubleClick(wnd w, Coord x = default, Coord y = default, bool nonClient = false) { clickEx(MButton.Left | MButton.DoubleClick, w, x, y, nonClient); } /// /// Left button double click at position x y. /// /// public static void doubleClick(Coord x, Coord y) { clickEx(MButton.Left | MButton.DoubleClick, x, y); } /// /// Left button double click. /// /// public static void doubleClick(bool useLastXY = false) { clickEx(MButton.Left | MButton.DoubleClick, useLastXY); } /// /// Left down (press and don't release) at position x y relative to window w. /// /// The return value can be used to auto-release the pressed button. Example: . /// public static MRelease leftDown(wnd w, Coord x = default, Coord y = default, bool nonClient = false) { return clickEx(MButton.Left | MButton.Down, w, x, y, nonClient); } /// /// Left button down (press and don't release) at position x y. /// /// The return value can be used to auto-release the pressed button. Example: . /// public static MRelease leftDown(Coord x, Coord y) { return clickEx(MButton.Left | MButton.Down, x, y); } /// /// Left button down (press and don't release). /// /// The return value can be used to auto-release the pressed button. Example: . /// public static MRelease leftDown(bool useLastXY = false) { return clickEx(MButton.Left | MButton.Down, useLastXY); } /// /// Left button up (release pressed button) at position x y relative to window w. /// /// public static void leftUp(wnd w, Coord x = default, Coord y = default, bool nonClient = false) { clickEx(MButton.Left | MButton.Up, w, x, y, nonClient); } /// /// Left button up (release pressed button) at position x y. /// /// public static void leftUp(Coord x, Coord y) { clickEx(MButton.Left | MButton.Up, x, y); } /// /// Left button up (release pressed button). /// /// public static void leftUp(bool useLastXY = false) { clickEx(MButton.Left | MButton.Up, useLastXY); } /// /// Right button down (press and don't release) at position x y relative to window w. /// /// The return value can be used to auto-release the pressed button. Example: . /// public static MRelease rightDown(wnd w, Coord x = default, Coord y = default, bool nonClient = false) { return clickEx(MButton.Right | MButton.Down, w, x, y, nonClient); } /// /// Right button down (press and don't release) at position x y. /// /// The return value can be used to auto-release the pressed button. Example: . /// public static MRelease rightDown(Coord x, Coord y) { return clickEx(MButton.Right | MButton.Down, x, y); } /// /// Right button down (press and don't release). /// /// The return value can be used to auto-release the pressed button. Example: . /// public static MRelease rightDown(bool useLastXY = false) { return clickEx(MButton.Right | MButton.Down, useLastXY); } /// /// Right button up (release pressed button) at position x y relative to window w. /// /// public static void rightUp(wnd w, Coord x = default, Coord y = default, bool nonClient = false) { clickEx(MButton.Right | MButton.Up, w, x, y, nonClient); } /// /// Right button up (release pressed button) at position x y. /// /// public static void rightUp(Coord x, Coord y) { clickEx(MButton.Right | MButton.Up, x, y); } /// /// Right button up (release pressed button). /// /// public static void rightUp(bool useLastXY = false) { clickEx(MButton.Right | MButton.Up, useLastXY); } /// /// Mouse wheel forward or backward. /// /// Number of wheel ticks forward (positive) or backward (negative). /// Horizontal wheel. /// /// Uses : . /// /// public static void wheel(double ticks, bool horizontal = false) { bool neg = ticks < 0; if (neg) ticks = -ticks; ticks *= 120; while (ticks > 0) { short t = (short)(ticks < 30000 ? Math.Ceiling(ticks) : 30000); //max 250 full ticks _SendRaw(horizontal ? Api.IMFlags.HWheel : Api.IMFlags.Wheel, 0, 0, neg ? -t : t); ticks -= t; } _Sleep(opt.mouse.ClickSleepFinally); } //rejected. Not so often used. It's easy to move(); wheel(). ///// ///// Mouse move and wheel. ///// //public static void wheel(Coord x, Coord y, double ticks, bool horizontal = false) { // move(x, y); // wheel(ticks, horizontal); //} ///// ///// Mouse move and wheel. ///// //public static void wheel(wnd w, Coord x, Coord y, double ticks, bool horizontal = false) { // move(w, x, y); // wheel(ticks, horizontal); //} /// /// Presses a mouse button in object o1, moves the mouse cursor to object o2 and releases the button. /// /// UI object (window, UI element, etc) where to press the mouse button. /// UI object where to release the mouse button. /// X offset in o1 rectangle. Default: center. /// Y offset in o1 rectangle. Default: center. /// X offset in o2 rectangle. Default: center. /// Y offset in o2 rectangle. Default: center. /// Mouse button. Default: left. /// Modifier keys (Ctrl etc). /// Wait this number of milliseconds after pressing the mouse button. /// The drag speed. See . /// public static void drag(MObject o1, MObject o2, Coord x1 = default, Coord y1 = default, Coord x2 = default, Coord y2 = default, MButton button = MButton.Left, KMod mod = 0, int sleep = 0, int speed = 5) { if (o2.Value is bool useLastXY) { //get mouse position before moving to o1 var p = _CoordToRelativeXY(x2, y2, useLastXY); o2 = default; x2 = p.x; y2 = p.y; } _Drag(o1, x1, y1, button, mod, sleep, speed, () => move(o2, x2, y2)); } /// /// Presses a mouse button in object obj, moves the mouse cursor by offset dx dy and releases the button. /// /// UI object (window, UI element, etc) where to press the mouse button. /// X offset in obj rectangle. Default: center. /// Y offset in obj rectangle. Default: center. /// X offset from the start position. /// Y offset from the start position. /// Mouse button. Default: left. /// Modifier keys (Ctrl etc). /// Wait this number of milliseconds after pressing the mouse button. /// The drag speed. See . /// public static void drag(MObject obj, Coord x, Coord y, int dx, int dy, MButton button = MButton.Left, KMod mod = 0, int sleep = 0, int speed = 5) { _Drag(obj, x, y, button, mod, sleep, speed, () => moveBy(dx, dy)); } /// /// Presses a mouse button in object obj, moves the mouse cursor using multiple recorded offsets and releases the button. /// /// UI object (window, UI element, etc) where to press the mouse button. /// X offset in obj rectangle. Default: center. /// Y offset in obj rectangle. Default: center. /// String containing multiple x y offsets from the start position. See . /// Mouse button. Default: left. /// Modifier keys (Ctrl etc). /// Wait this number of milliseconds after pressing the mouse button. /// public static void drag(MObject obj, Coord x, Coord y, string offsets, MButton button = MButton.Left, KMod mod = 0, int sleep = 0) { _Drag(obj, x, y, button, mod, sleep, 0, () => moveBy(offsets)); } static void _Drag(MObject from, Coord x1, Coord y1, MButton button, KMod mod, int sleep, int speed, Action action) { if (button == 0) button = MButton.Left; else if (button != (button & (MButton.Left | MButton.Right | MButton.Middle | MButton.X1 | MButton.X2))) throw new ArgumentException(null, nameof(button)); if ((uint)sleep >= 10000) throw new ArgumentException(null, nameof(sleep)); if ((uint)speed >= 10000) throw new ArgumentException(null, nameof(speed)); int speed0 = opt.mouse.MoveSpeed; bool isMod = false, isButton = false; try { clickEx(button | MButton.Down, from, x1, y1); isButton = true; if (sleep > 0) wait.ms(sleep); _SendMod(true); //note: after button down. If before, it could eg Ctrl+select multiple objects instead of one. isMod = true; opt.mouse.MoveSpeed = speed; action(); } finally { if (isButton) clickEx(button | MButton.Up, useLastXY: true); opt.mouse.MoveSpeed = speed0; if (isMod) _SendMod(false); } void _SendMod(bool down) { if (mod == 0) return; var k = new keys(opt.key); if (mod.Has(KMod.Ctrl)) k.AddKey(KKey.Ctrl, down); if (mod.Has(KMod.Shift)) k.AddKey(KKey.Shift, down); if (mod.Has(KMod.Alt)) k.AddKey(KKey.Alt, down); if (mod.Has(KMod.Win)) k.AddKey(KKey.Win, down); k.SendNow(); //and sleeps opt.key.SleepFinally (default 10) } } //not used ///// ///// Releases mouse buttons pressed by this thread (t_pressedButtons). ///// ///// If not null, and XY is different, moves to this point. Used for reliability. //static void _ReleaseButtons(POINT? p = null) //{ // var b = t_pressedButtons; // if(0 != (b & MButtons.Left)) _Click(MButton.Left | MButton.Up, p); // if(0 != (b & MButtons.Right)) _Click(MButton.Right | MButton.Up, p); // if(0 != (b & MButtons.Middle)) _Click(MButton.Middle | MButton.Up, p); // if(0 != (b & MButtons.X1)) _Click(MButton.X1 | MButton.Up, p); // if(0 != (b & MButtons.X2)) _Click(MButton.X2 | MButton.Up, p); //} //rejected: finally release script-pressed buttons, especially on exception. Instead let use code: using(mouse.leftDown(...)), it auto-releases pressed button. /// /// Returns true if some mouse buttons are pressed. /// /// Return true if some of these buttons are down. Default: any. /// /// Uses API GetAsyncKeyState. /// When processing user input in UI code (forms, WPF), instead use class or .NET functions. They use API GetKeyState. /// When mouse left and right buttons are swapped, gets logical state, not physical. /// /// public static bool isPressed(MButtons buttons = MButtons.Left | MButtons.Right | MButtons.Middle | MButtons.X1 | MButtons.X2) { if (0 != (buttons & MButtons.Left) && keys.isPressed(KKey.MouseLeft)) return true; if (0 != (buttons & MButtons.Right) && keys.isPressed(KKey.MouseRight)) return true; if (0 != (buttons & MButtons.Middle) && keys.isPressed(KKey.MouseMiddle)) return true; if (0 != (buttons & MButtons.X1) && keys.isPressed(KKey.MouseX1)) return true; if (0 != (buttons & MButtons.X2) && keys.isPressed(KKey.MouseX2)) return true; return false; } //rejected: not useful. ///// ///// Returns a value indicating which mouse buttons are pressed. ///// ///// Check only these buttons. Default: all. ///// See . //public static MButtons buttons(MButtons buttons = MButtons.Left | MButtons.Right | MButtons.Middle | MButtons.X1 | MButtons.X2) //{ // MButtons R = 0; // if(0 != (buttons & MButtons.Left) && keys.isKey(KKey.MouseLeft)) R |= MButtons.Left; // if(0 != (buttons & MButtons.Right) && keys.isKey(KKey.MouseRight)) R |= MButtons.Right; // if(0 != (buttons & MButtons.Middle) && keys.isKey(KKey.MouseMiddle)) R |= MButtons.Middle; // if(0 != (buttons & MButtons.X1) && keys.isKey(KKey.MouseX1)) return R |= MButtons.X1; // if(0 != (buttons & MButtons.X2) && keys.isKey(KKey.MouseX2)) return R |= MButtons.X2; // return R; //} //rejected: rarely used. Can use IsPressed. ///// ///// Returns true if the left mouse button is pressed. ///// ///// See . //public static bool isLeft => keys.isPressed(KKey.MouseLeft); ///// ///// Returns true if the right mouse button is pressed. ///// ///// See . //public static bool isRight => keys.isPressed(KKey.MouseRight); /// /// Waits while some mouse buttons are pressed. See . /// /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). Default 0. /// Wait only for these buttons. Default - all. /// Returns true. On timeout returns false if timeout is negative; else exception. /// timeout time has expired (if > 0). /// public static bool waitForNoButtonsPressed(Seconds timeout = default, MButtons buttons = MButtons.Left | MButtons.Right | MButtons.Middle | MButtons.X1 | MButtons.X2) { return keys.waitForNoModifierKeysAndMouseButtons(timeout, 0, buttons); } /// /// Waits while some buttons are pressed, except those pressed by a class function in this thread. /// Does nothing if option Relaxed is true. /// internal static void WaitForNoButtonsPressed_() { //not public, because we have WaitForNoButtonsPressed, which is unaware about script-pressed buttons, and don't need this awareness because the script author knows what is pressed by that script if (opt.mouse.Relaxed) return; var mb = (MButtons.Left | MButtons.Right | MButtons.Middle | MButtons.X1 | MButtons.X2) & ~t_pressedButtons; if (waitForNoButtonsPressed(-2, mb)) return; //initially was 5 s, but users don't wait so long, and then wonder why it does not work. It's documented in OMouse.Relaxed. print.warning("The mouse-move function waits because a mouse button is pressed by the user. This is to prevent unintended drag-and-drop. To disable this in the script: opt.mouse.Relaxed = true;"); waitForNoButtonsPressed(0, mb); } /// /// Waits for button-down or button-up event of the specified mouse button or buttons. /// /// Returns true. On timeout returns false if timeout is negative; else exception. /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// Mouse button. If several buttons specified, waits for any of them. /// Wait for button-up event. /// Make the event invisible to other apps. If up is true, makes the down event invisible too, if it comes while waiting for the up event. /// button is 0. /// timeout time has expired (if > 0). /// /// Unlike , waits for down or up event, not for button state. /// Uses low-level mouse hook. /// Ignores mouse events injected by functions of this library. /// /// /// /// public static bool waitForClick(Seconds timeout, MButtons button, bool up = false, bool block = false) { if (button == 0) throw new ArgumentException(); return 0 != _WaitForClick(timeout, button, up, block); } /// /// Waits for button-down or button-up event of any mouse button, and gets the button code. /// /// Returns the button code. On timeout returns 0 if timeout is negative; else exception. /// timeout time has expired (if > 0). /// /// /// /// public static MButtons waitForClick(Seconds timeout, bool up = false, bool block = false) { return _WaitForClick(timeout, 0, up, block); } static MButtons _WaitForClick(Seconds timeout, MButtons button, bool up, bool block) { //info: this and related functions use similar code as keys._WaitForKey. MButtons R = 0; using (WindowsHook.Mouse(x => { MButtons b = 0; switch (x.Event) { case HookData.MouseEvent.LeftButton: b = MButtons.Left; break; case HookData.MouseEvent.RightButton: b = MButtons.Right; break; case HookData.MouseEvent.MiddleButton: b = MButtons.Middle; break; case HookData.MouseEvent.X1Button: b = MButtons.X1; break; case HookData.MouseEvent.X2Button: b = MButtons.X2; break; } if (b == 0) return; if (button != 0 && !button.Has(b)) return; if (x.IsButtonUp != up) { if (up && block) { //button down when we are waiting for up. If block, now block down too. if (button == 0) button = b; x.BlockEvent(); } return; } R = b; if (block) x.BlockEvent(); })) wait.doEventsUntil(timeout, () => R != 0); return R; } //FUTURE: // waitForWheel(Seconds timeout, bool? forward, bool block = false) // waitForMouseMove, waitForMouseStop. // In QM2 these functions were created because somebody asked, but I don't use. /// /// Waits for a standard mouse cursor (pointer) visible. /// /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// Id of a standard cursor. /// Wait until this cursor disappears. /// Returns true. On timeout returns false if timeout is negative; else exception. /// timeout time has expired (if > 0). public static bool waitForCursor(Seconds timeout, MCursor cursor, bool not = false) { IntPtr hcur = Api.LoadCursor(default, cursor); if (hcur == default) throw new AuException(0, "*load cursor"); return wait.until(timeout, () => (MouseCursor.GetCurrentVisibleCursor(out var c) && c == hcur) ^ not); } /// /// Waits for a nonstandard mouse cursor (pointer) visible. /// /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// Cursor hash, as returned by . /// Wait until this cursor disappears. /// Returns true. On timeout returns false if timeout is negative; else exception. /// timeout time has expired (if > 0). public static bool waitForCursor(Seconds timeout, long cursorHash, bool not = false) { if (cursorHash == 0) throw new ArgumentException(); return wait.until(timeout, () => (MouseCursor.GetCurrentVisibleCursor(out var c) && MouseCursor.Hash(c) == cursorHash) ^ not); } //TODO2: example. Cookbook contains example, but no info how to get the hash (MouseCursor.GetCurrentVisibleCursor + MouseCursor.Hash). Maybe even need a tool. //TODO2: wait for any in list. /// /// Posts mouse-click messages to the window. /// /// Window or control. /// X coordinate in w client area or rect. Default - center. Examples: 10, ^10 (reverse), .5f (fraction). /// Y coordinate in w client area or rect. Default - center. /// Can specify the left (default), right or middle button. Also flag for double-click, press or release. /// A rectangle in w client area. If null (default), x y are relative to the client area. /// Invalid window. /// Unsupported button specified. /// /// Does not move the mouse. /// Does not wait until the target application finishes processing the message. /// Works not with all windows. /// public static void postClick(wnd w, Coord x = default, Coord y = default, MButton button = MButton.Left, RECT? rect = null) { RECT r; if (rect != null) { r = rect.Value; w.ThrowIfInvalid(); } else { if (!w.GetClientRect(out r)) w.ThrowUseNative(); } PostClick_(w, r, x, y, button); } internal static void PostClick_(wnd w, RECT r, Coord x = default, Coord y = default, MButton button = MButton.Left) { MButton mask = MButton.Down | MButton.Up | MButton.DoubleClick, b = button & ~mask, dud = button & mask; if (b == 0) b = MButton.Left; int m = b switch { MButton.Left => Api.WM_LBUTTONDOWN, MButton.Right => Api.WM_RBUTTONDOWN, MButton.Middle => Api.WM_MBUTTONDOWN, _ => throw new ArgumentException("supported buttons: left, right, middle") }; if (dud is not (0 or MButton.Down or MButton.Up or MButton.DoubleClick)) throw new ArgumentException(); POINT point = Coord.NormalizeInRect(x, y, r, centerIfEmpty: true); //if the control is mouse-transparent, use its ancestor. Example: the color selection controls in the classic color dialog. if (w.HasStyle(WS.CHILD)) { var w2 = w; var ps = point; w.MapClientToScreen(ref ps); while (!w2.Is0 && Api.HTTRANSPARENT == w2.Send(Api.WM_NCHITTEST, 0, Math2.MakeLparam(ps))) w2 = w2.Get.DirectParent; if (w2 != w && !w2.Is0) { w.MapClientToClientOf(w2, ref point); w = w2; } } using var workaround = new ButtonPostClickWorkaround_(w); nint xy = Math2.MakeLparam(point); nint mk = 0; if (keys.isCtrl) mk |= Api.MK_CONTROL; if (keys.isShift) mk |= Api.MK_SHIFT; nint mk1 = mk; if (dud != MButton.Up) mk1 |= b switch { MButton.Left => Api.MK_LBUTTON, MButton.Right => Api.MK_RBUTTON, _ => Api.MK_MBUTTON }; if (dud != MButton.Up) w.Post(m, mk1, xy); if (dud != MButton.Down) { w.Post(Api.WM_MOUSEMOVE, mk1, xy); w.Post(m + 1, mk, xy); } if (dud == MButton.DoubleClick) { w.Post(m + 2, mk1, xy); w.Post(m + 1, mk, xy); } //_MinimalSleep(); //don't need. Eg elm.Invoke() does not wait too. //never mind: support nonclient (WM_NCRBUTTONDOWN etc) } /// /// Workaround for the documented BM_CLICK/WM_LBUTTONDOWN bug of classic button controls: randomly fails if inactive window. /// If c is a button in a dialog box, posts WM_ACTIVATE messages to the dialog box. /// internal ref struct ButtonPostClickWorkaround_ { readonly wnd _w; public ButtonPostClickWorkaround_(wnd c) { //this func is fast, but slower is elm.wndcontainer (to get c) and JIT _w = default; var w = c.Window; //not c.DirectParent. Eg in taskdialog it is not #32770. The postclick/invoke worked without this workaround in all tested top-level non-#32770 windows, with or without an intermediate #32770. if (w != c && !w.IsActive && w.ClassNameIs("#32770") && c.CommonControlType == WControlType.Button) { w.Post(Api.WM_ACTIVATE, 1); _w = w; } } public void Dispose() { if (!_w.Is0) _w.Post(Api.WM_ACTIVATE); } } } ================================================ FILE: Au/Input/mouse_types.cs ================================================ namespace Au.Types; /// /// button parameter type for and similar functions. /// /// /// There are two groups of values: /// 1. Button (Left, Right, Middle, X1, X2). Default or 0: Left. /// 2. Action (Down, Up, DoubleClick). Default: click. /// /// Multiple values from the same group cannot be combined. For example Left|Right is invalid. /// Values from different groups can be combined. For example Right|Down. /// [Flags] public enum MButton { /// The left button. Left = 1, /// The right button. Right = 2, /// The middle button. Middle = 4, /// The 4-th button. X1 = 8, /// The 5-th button. X2 = 16, //rejected: not necessary. Can be confusing. ///// ///// Click (press and release). ///// This is default. Value 0. ///// //Click = 0, /// (flag) Press and don't release. Down = 32, /// (flag) Don't press, only release. Up = 64, /// (flag) Double-click. DoubleClick = 128, } /// /// Flags for mouse buttons. /// Used with functions that check mouse button states (pressed or not). /// /// /// The values are the same as , therefore can be cast to/from. /// [Flags] public enum MButtons { /// The left button. Left = 0x00100000, /// The right button. Right = 0x00200000, /// The middle button. Middle = 0x00400000, /// The 4-th button. X1 = 0x00800000, /// The 5-th button. X2 = 0x01000000, } /// /// At the end of using(...) { ... } block releases mouse buttons pressed by the function that returned this variable. See example. /// /// /// Drag and drop: start at x=8 y=8, move 20 pixels down, drop. /// /// public struct MRelease : IDisposable { MButton _buttons; /// public static implicit operator MRelease(MButton b) => new MRelease() { _buttons = b }; /// /// Releases mouse buttons pressed by the function that returned this variable. /// public void Dispose() { if (0 == (_buttons & MButton.Down)) return; if (0 != (_buttons & MButton.Left)) mouse.clickEx(MButton.Left | MButton.Up, true); if (0 != (_buttons & MButton.Right)) mouse.clickEx(MButton.Right | MButton.Up, true); if (0 != (_buttons & MButton.Middle)) mouse.clickEx(MButton.Middle | MButton.Up, true); if (0 != (_buttons & MButton.X1)) mouse.clickEx(MButton.X1 | MButton.Up, true); if (0 != (_buttons & MButton.X2)) mouse.clickEx(MButton.X2 | MButton.Up, true); } } /// /// Standard cursor ids. /// Used with . /// public enum MCursor { /// Standard arrow. Arrow = 32512, /// I-beam (text editing). IBeam = 32513, /// Hourglass. Wait = 32514, /// Crosshair. Cross = 32515, /// Vertical arrow. UpArrow = 32516, /// Double-pointed arrow pointing northwest and southeast. SizeNWSE = 32642, /// Double-pointed arrow pointing northeast and southwest. SizeNESW = 32643, /// Double-pointed arrow pointing west and east. SizeWE = 32644, /// Double-pointed arrow pointing north and south. SizeNS = 32645, /// Four-pointed arrow pointing north, south, east, and west. SizeAll = 32646, /// Slashed circle. No = 32648, /// Hand. Hand = 32649, /// Standard arrow and small hourglass. AppStarting = 32650, /// Arrow and question mark. Help = 32651, } /// /// This type is used for parameters of functions that accept multiple types of UI objects (window, UI element, screen, etc). /// /// /// Has implicit conversions from , , , , and bool (relative coordinates). /// Also has static functions to specify more parameters. /// public struct MObject { object _o; MObject(object o) => _o = o; /// public object Value => _o; /// /// Allows to specify coordinates in the client area of a window or control. /// /// The window handle is 0. /// public static implicit operator MObject(wnd w) { w.ThrowIf0(); return new(w); } /// /// Allows to specify coordinates in the rectangle of a UI element. /// /// public static implicit operator MObject(elm e) => new(Not_.NullRet(e)); /// /// Allows to specify coordinates in the rectangle of an image found in a window etc. /// /// public static implicit operator MObject(uiimage i) => new(Not_.NullRet(i)); /// /// Allows to specify coordinates in a rectangle anywhere on screen. /// /// public static implicit operator MObject(RECT r) => new(r); /// /// Allows to specify coordinates in a screen. /// /// public static implicit operator MObject(screen s) => new((s, false)); /// /// Allows to specify coordinates relative to or . /// public static implicit operator MObject(bool useLastXY) => new(useLastXY); /// /// Allows to specify coordinates in a screen, either in the work area or in entire rectangle. /// /// /// /// public static MObject Screen(screen s, bool workArea) => new((s, workArea)); /// /// Allows to specify coordinates in a window or control, either in the client area or in entire rectangle. /// /// The window handle is 0. public static MObject Window(wnd w, bool nonClient) { w.ThrowIf0(); return new((w, true)); } /// /// Allows to specify coordinates in a rectangle in the client area of a window or control. /// /// The window handle is 0. public static MObject RectInWindow(wnd w, RECT r) { w.ThrowIf0(); return new((w, r)); } } ================================================ FILE: Au/Internal/ActCtx_.cs ================================================ namespace Au.More; /// /// Activates our manifest which tells to use comctl32.dll version 6. /// The manifest is embedded in this dll, resource id 2. /// internal sealed class ActCtx_ : IDisposable { IntPtr _cookie; public static ActCtx_ Activate() { if (s_actCtx.Handle == default || !Api.ActivateActCtx(s_actCtx.Handle, out var cookie)) return default; return new ActCtx_() { _cookie = cookie }; } public void Dispose() { if (_cookie != default) { Api.DeactivateActCtx(0, _cookie); _cookie = default; } } static _ActCtx s_actCtx = new _ActCtx(); class _ActCtx { public IntPtr Handle; public _ActCtx() { Api.ACTCTX a = default; a.cbSize = Api.SizeOf(); #if true //the manifest is in AuCpp.dll a.dwFlags = Api.ACTCTX_FLAG_RESOURCE_NAME_VALID | Api.ACTCTX_FLAG_HMODULE_VALID; a.hModule = Cpp.Cpp_ModuleHandle(); #else //old code. The manifest was in Au.dll. If , Location returns "". a.dwFlags = Api.ACTCTX_FLAG_RESOURCE_NAME_VALID; a.lpSource = Assembly.GetExecutingAssembly().Location; #endif a.lpResourceName = (IntPtr)2; var h = Api.CreateActCtx(a); if (h != (IntPtr)(-1)) Handle = h; } ~_ActCtx() { if (Handle != default) { Api.ReleaseActCtx(Handle); Handle = default; } } } } ================================================ FILE: Au/Internal/ArrayBuilder_.cs ================================================ //CONSIDER: System.Buffers.ArrayPool. namespace Au.More; /// /// Like List or StringBuilder, used as a temporary variable-size array to create final fixed-size array. /// To avoid much garbage (and many reallocations when growing), uses native memory heap. See . /// Must be explicitly disposed to free the native memory. Does not have a finalizer because is struct (to avoid garbage). /// Does not support reference types. Does not call T.Dispose. /// //[DebuggerStepThrough] internal unsafe struct ArrayBuilder_ : IDisposable where T : unmanaged { T* _p; int _len, _cap; static int s_minCap; static ArrayBuilder_() { var r = 16384 / sizeof(T); //above 16384 the memory allocation API become >=2 times slower if (r > 500) r = 500; else if (r < 8) r = 8; s_minCap = r; //info: 500 is optimal for getting all top-level windows (and invisible) as ArrayBuilder_. // Normally there are 200-400 windows on my PC, rarely > 500. } public void Dispose() => Free(); /// /// Gets array memory address (address of element 0). /// public T* Ptr => _p; /// /// Gets the number of elements. /// public int Count => _len; /// /// Gets the number of bytes in the array (Count*sizeof(T)). /// public int ByteCount => _len * sizeof(T); /// /// Gets or sets the total number of elements (not bytes) the internal memory can hold without resizing. /// /// (the set function) value less than Count. Instead use ReAlloc or Free. public int Capacity { get => _cap; set { if (value != _cap) { if (value < _len) throw new ArgumentOutOfRangeException(); MemoryUtil.ReAlloc(ref _p, value); _cap = value; } } } /// /// Allocates count elements. Sets Count=count. /// Frees previously allocated memory. /// Returns memory address (address of element 0). /// /// Element count. /// Set all bytes = 0. If false, the memory is uninitialized, ie random byte values. Default true. Slower when true. /// Set Capacity = count. If false, allocates more if count is less than the minimal capacity for this type. public T* Alloc(int count, bool zeroInit = true, bool noExtra = false) { if (_cap != 0) Free(); int cap = count; if (cap < s_minCap && !noExtra) cap = s_minCap; _p = MemoryUtil.Alloc(cap, zeroInit); _cap = cap; _len = count; return _p; } /// /// Adds or removes elements at the end. Sets Count=count. /// Preserves Math.Min(Count, count) existing elements. /// Returns memory address (address of element 0). /// /// New element count. /// Set all added bytes = 0. If false, the added memory is uninitialized, ie random byte values. Default true. Slower when true. /// Set Capacity = count. If false, allocates more if count is less than the minimal capacity for this type. /// /// The new memory usually is at a new location. The preserved elements are copied there. /// Sets Count=count. To allocate more memory without changing Count, change Capacity instead. /// public T* ReAlloc(int count, bool zeroInit = true, bool noExtra = false) { int cap = count; if (cap < s_minCap && !noExtra) cap = s_minCap; MemoryUtil.ReAlloc(ref _p, cap, zeroInit); _cap = cap; _len = count; return _p; } /// /// Frees memory. Sets Count and Capacity = 0. /// public void Free() { if (_cap == 0) return; _len = _cap = 0; var p = _p; _p = null; MemoryUtil.Free(p); } /// /// Adds one element. /// The same as Add, but uses in. Use to avoid copying values of big types. /// public void AddR(in T value) { if (_len == _cap) _EnsureCapacity(); _p[_len++] = value; } /// /// Adds one element. /// public void Add(T value) { if (_len == _cap) _EnsureCapacity(); _p[_len++] = value; } /// /// Adds one zero-inited element and returns its reference. /// public ref T Add() { if (_len == _cap) _EnsureCapacity(); ref T r = ref _p[_len]; r = default; _len++; return ref r; } /// /// Capacity = Math.Max(_cap * 2, s_minCap). /// void _EnsureCapacity() { Capacity = Math.Max(_cap * 2, s_minCap); } /// /// Gets element reference. /// /// Element index. /// public ref T this[int i] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { if ((uint)i >= (uint)_len) _ThrowBadIndex(); return ref _p[i]; } } [MethodImpl(MethodImplOptions.NoInlining)] static void _ThrowBadIndex() { throw new IndexOutOfRangeException(); } /// /// Copies elements to a new managed array. /// public T[] ToArray() { if (_len == 0) return []; var r = new T[_len]; for (int i = 0; i < r.Length; i++) r[i] = _p[i]; return r; } } ================================================ FILE: Au/Internal/AssemblyUtil_.cs ================================================ using System.Runtime.Loader; namespace Au.More; /// /// Assembly functions. /// internal static class AssemblyUtil_ { public static Assembly GetEntryAssembly() { var r = Assembly.GetEntryAssembly(); if (r == null) { //info: null in miniProgram on AssemblyLoadContext.Default.Resolving event when the script is like: // 'class Program : SomeType { static Main() { } }' where SomeType is the assembly being resolved. //Debug_.Print("Assembly.GetEntryAssembly null"); if (script.role == SRole.MiniProgram) { var s = "~" + script.name; foreach (var v in AssemblyLoadContext.Default.Assemblies) if (v.GetName().Name == s) return v; } } return r; } /// /// Returns true if the build configuration of the assembly is Debug. Returns false if Release (optimized). /// /// /// Returns true if the assembly has and its IsJITTrackingEnabled is true. /// public static bool IsDebug(Assembly a) => a?.GetCustomAttribute()?.IsJITTrackingEnabled ?? false; //IsJITTrackingEnabled depends on config, but not 100% reliable, eg may be changed explicitly in source code (maybe IsJITOptimizerDisabled too). //IsJITOptimizerDisabled depends on 'Optimize code' checkbox in project Properties, regardless of config. //note: GetEntryAssembly returns null in func called by host through coreclr_create_delegate. /// /// Returns flags for loaded assemblies: 1 System.Windows.Forms, 2 WindowsBase (WPF). /// internal static int IsLoadedWinformsWpf() { if (s_isLoadedWinformsWpf == 0) { lock ("zjm5R47f7UOmgyHUVZaf1w") { if (s_isLoadedWinformsWpf == 0) { var ad = AppDomain.CurrentDomain; var a = ad.GetAssemblies(); foreach (var v in a) { _FlagFromName(v); if (s_isLoadedWinformsWpf == 3) return 3; } ad.AssemblyLoad += (_, x) => _FlagFromName(x.LoadedAssembly); s_isLoadedWinformsWpf |= 0x100; } } } return s_isLoadedWinformsWpf & 3; void _FlagFromName(Assembly a) { string s = a.FullName; //fast, cached. GetName can be slow because not cached. if (0 == (s_isLoadedWinformsWpf & 1) && s.Starts("System.Windows.Forms,")) s_isLoadedWinformsWpf |= 1; else if (0 == (s_isLoadedWinformsWpf & 2) && s.Starts("WindowsBase,")) s_isLoadedWinformsWpf |= 2; } } static volatile int s_isLoadedWinformsWpf; } ================================================ FILE: Au/Internal/AttachThreadInput_.cs ================================================ namespace Au.More; /// /// Calls API AttachThreadInput to attach/detach thread input. /// Constructor attaches thread input of this thread to that of the specified thread. Dispose detaches. /// internal struct AttachThreadInput_ : IDisposable { int _tidThis, _tidAttach; /// /// Attaches thread input of this thread to that of the specified thread. /// public AttachThreadInput_(int idThreadAttachTo, out bool succeeded) { _tidThis = Api.GetCurrentThreadId(); succeeded = Api.AttachThreadInput(_tidThis, idThreadAttachTo, true); _tidAttach = succeeded ? idThreadAttachTo : 0; } /// /// Detaches thread input. /// public void Dispose() { if (_tidAttach != 0) { Api.AttachThreadInput(_tidThis, _tidAttach, false); _tidAttach = 0; } } /// /// Returns true if AttachThreadInput succeeded and this variable is not disposed. /// public bool IsAttached => _tidAttach != 0; } ================================================ FILE: Au/Internal/Debug_.cs ================================================ namespace Au.More; /// /// Functions useful to debug code. /// /// /// The Debug_.PrintX functions write to the same output as , not to the trace listeners like etc do. Also they add caller's name, file and line number. /// Functions Print, PrintIf, PrintFunc and Dialog work only if DEBUG is defined, which normally is when the caller project is in Debug configuration. Else they are not called, and arguments not evaluated at run time. This is because they have [ConditionalAttribute("DEBUG")]. /// Note: when used in a library, the above functions depend on DEBUG of the library project and not on DEBUG of the consumer project of the library. For example, the library may be in Release configuration even if its consumer project is in Debug configuration. If your library wants to show some info only if its consumer project is in Debug config, instead you can use code like if(opt.warnings.Verbose) print.warning("text");; see , . /// internal static class Debug_ { static void _Print(object text, string f_, int l_, string m_) { string s = print.util.toString(text), fname = pathname.getName(f_), st; int i = s.Find("\r\n at "); if (i >= 0) { st = s[(i + 2)..]; s = s[..i]; } else st = new StackTrace(2, true).ToString(); s = $"<>Debug: {m_} ({fname}:{l_}): {s} \r\n{st}"; _Print2(s); } static void _Print2(object o) { string s = o?.ToString(); if (UseQM2) print.qm2.write(s); else print.it(s); } internal static bool UseQM2; /// /// Calls to show some debug info. Also shows current function name/file/line. /// Works only if DEBUG is defined. Read more in class help. /// The 3 optional arguments are not used explicitly. /// Text can contain output tags. /// [Conditional("DEBUG")] public static void Print(object text, [CallerFilePath] string f_ = null, [CallerLineNumber] int l_ = 0, [CallerMemberName] string m_ = null) => _Print(text, f_, l_, m_); /// /// If condition is true, calls to show some debug info. Also shows current function name/file/line. /// Works only if DEBUG is defined. Read more in class help. /// If text null, uses [CallerArgumentExpression("condition")]. Other optional parameters are not used explicitly. /// If text starts with "<>", it can contain output tags. /// [Conditional("DEBUG")] public static void PrintIf(bool condition, object text = null, [CallerFilePath] string f_ = null, [CallerLineNumber] int l_ = 0, [CallerMemberName] string m_ = null, [CallerArgumentExpression("condition")] string ae_ = null) { if (condition) _Print(text ?? ae_, f_, l_, m_); } /// /// Calls with current function name. /// Works only if DEBUG is defined. Read more in class help. /// The optional argument is not used explicitly. /// [Conditional("DEBUG")] public static void PrintFunc([CallerMemberName] string m_ = null) => _Print2(m_); /// /// If DEBUG defined, prints lastError.message. Only if condition true (default). /// [Conditional("DEBUG")] public static void PrintNativeError(bool condition = true, [CallerFilePath] string f_ = null, [CallerLineNumber] int l_ = 0, [CallerMemberName] string m_ = null) { if (condition) _Print(lastError.message, f_, l_, m_); } /// /// If DEBUG defined, prints lastError.messageFor(code). /// [Conditional("DEBUG")] public static void PrintNativeError(int code, [CallerFilePath] string f_ = null, [CallerLineNumber] int l_ = 0, [CallerMemberName] string m_ = null) => _Print(lastError.messageFor(code), f_, l_, m_); /// /// Calls to show some debug info. /// Works only if DEBUG is defined. Read more in class help. /// The 3 optional arguments are not used explicitly. /// [Conditional("DEBUG")] public static void Dialog(object text, [CallerFilePath] string f_ = null, [CallerLineNumber] int l_ = 0, [CallerMemberName] string m_ = null) { string s = print.util.toString(text); dialog.show("Debug", s, flags: DFlags.ExpandDown, expandedText: $"{m_} ({pathname.getName(f_)}:{l_})"); } //rejected: use if(opt.warnings.Verbose) dialog.showWarning(...). It adds stack trace. ///// ///// If opt.warnings.Verbose, calls with text and stack trace. ///// Read more in class help. ///// //[MethodImpl(MethodImplOptions.NoInlining)] //public static void DialogOpt(string text) //{ // if(!opt.warnings.Verbose) return; // var x = new StackTrace(1, true); // dialog.show("Debug", text, flags: DFlags.ExpandDown | DFlags.Wider, expandedText: x.ToString()); //} //rejected: Not used in this library. Not useful for debug because don't show the stack trace. Instead use print.warning; it supports prefix "Debug: ", "Note: ", "Info :"; it also supports disabling warnings etc. ///// ///// If opt.warnings.Verbose, calls . ///// Read more in class help. ///// //public static void PrintOpt(string text) //{ // if(opt.warnings.Verbose) _Print("Debug: " + text); //} //rejected: Don't need multiple warning functions. Now print.warning does not show more than 1 warning/second if opt.warnings.Verbose is false. ///// ///// If opt.warnings.Verbose, calls . ///// Read more in class help. ///// //[MethodImpl(MethodImplOptions.NoInlining)] //public static void WarningOpt(string text) //{ // if(opt.warnings.Verbose) print.warning(text, 1); //} /// /// Checks flags and throws if some flags are invalid. The error message includes valid flag names. /// /// Flags to check. /// Valid flags. /// /// Can be used in functions that have an enum flags parameter but not all passed flags are valid for that function or object state. /// Does nothing if !opt.warnings.Verbose. /// When flags are valid, this function is fast. /// public static unsafe void CheckFlagsOpt(T flags, T goodFlags) where T : unmanaged, Enum { //FUTURE: if this is really often useful, make it public. If not used - remove. Debug.Assert(sizeof(T) == 4); int a = *(int*)&flags; int b = *(int*)&goodFlags; if (a != (a & b)) _CheckFlagsOpt(typeof(T), b); } [MethodImpl(MethodImplOptions.NoInlining)] static void _CheckFlagsOpt(Type t, int goodFlags) { if (!opt.warnings.Verbose) return; if (!t.IsEnum) throw new ArgumentException("Bad type."); var s = new StringBuilder("Invalid flags. Only these flags can be used: "); bool added = false; for (int i = 1; i != 0; i <<= 1) { if (0 == (i & goodFlags)) continue; if (added) s.Append(", "); else added = true; s.Append(t.GetEnumName(i)); } s.Append('.'); //print.warning(s.ToString(), 1); throw new ArgumentException(s.ToString()); } /// /// Returns true if using Debug configuration of Au.dll. /// public static bool IsAuDebugConfiguration { get { #if DEBUG return true; #else return false; #endif } } //CONSIDER: move the MemoryX functions to perf as public. /// /// Calls Marshal.AddRef(obj), then calls/returns Marshal.Release(obj). /// public static int GetComObjRefCount(IntPtr obj) { Marshal.AddRef(obj); return Marshal.Release(obj); } /// /// Returns managed memory size as formatted string. Uses GC.GetTotalMemory. /// /// Get the difference from previous call to MemorySetAnchor_. public static string MemoryGet(bool fromAnchor = true) { var mem = GC.GetTotalMemory(false); //if(s_mem0 == 0) s_mem0 = mem; if (fromAnchor) mem -= s_mem0; return (mem / 1024d / 1024d).ToS("F3"); } static long s_mem0; /// /// Prints managed memory size. Uses GC.GetTotalMemory. /// /// Get the difference from previous call to MemorySetAnchor_. public static void MemoryPrint(bool fromAnchor = true) => _Print2(MemoryGet(fromAnchor)); /// /// Memorizes current managed memory size, so that next call to another MemoryX function with fromAnchor=true (default) will get memory size difference from current memory size. /// public static void MemorySetAnchor() { s_mem0 = GC.GetTotalMemory(false); } ///// ///// Temporarily suspends GC collections if possible. Restores in Dispose. ///// //public struct NoGcRegion : IDisposable { // bool _restore, _print; // /// // /// Suspends GC collections if possible. // /// Does nothing in 32-bit process. // /// // /// Recommended 100_000_000. Still works if 200_000_000, but fails if 300_000_000. Not tested in 32-bit process. // /// Let Dispose print size of managed memory added since ctor. // public NoGcRegion(long memSize, bool print = true) { // _restore = false; // _print = print; // if (osVersion.is32BitProcess) return; // if (_print) Debug_.MemorySetAnchor_(); // //print.it(System.Runtime.GCSettings.LatencyMode); // try { _restore = GC.TryStartNoGCRegion(memSize); } // catch (InvalidOperationException ex) { Debug_.Print(ex); } // } // /// // /// Restores suspended GC collections. // /// // public void Dispose() { // if (_restore) { // _restore = false; // //print.it(System.Runtime.GCSettings.LatencyMode == System.Runtime.GCLatencyMode.NoGCRegion); // //if(System.Runtime.GCSettings.LatencyMode == System.Runtime.GCLatencyMode.NoGCRegion) GC.EndNoGCRegion(); // try { GC.EndNoGCRegion(); } //note: need to call even if not in nogc region (then exception); else TryStartNoGCRegion will throw exception. // catch (InvalidOperationException ex) { Debug_.Print(ex); } // if (_print) Debug_.MemoryPrint_(); // ThreadPool.QueueUserWorkItem(_ => GC.Collect()); // } // } //} /// /// Prints assemblies already loaded or/and loaded in the future. /// public static void PrintLoadedAssemblies(bool now, bool future, bool stackTrace = false) { if (now) { var a = AppDomain.CurrentDomain.GetAssemblies(); _Print2("-- now --"); foreach (var v in a) _Print2("# " + v.FullName); } if (future) { if (stackTrace) new StackTrace(1, true); //load assemblies used by stack trace _Print2("-- future --"); AppDomain.CurrentDomain.AssemblyLoad += (object sender, AssemblyLoadEventArgs e) => { _Print2("# " + e.LoadedAssembly.FullName); if (stackTrace) _Print2(new StackTrace(1, true)); }; //var stack = new Stack(); } } } ================================================ FILE: Au/Internal/GC_.cs ================================================ namespace Au.More { /// /// extensions. /// static class GC_ { static readonly ConditionalWeakTable s_table = new(); /// /// Calls . Later, when object obj is garbage-collected, will call . /// /// An object of any type. /// Unmanaged memory size. It is passed to GC.AddMemoryPressure and GC.RemoveMemoryPressure. public static void AddObjectMemoryPressure(object obj, long size) { GC.AddMemoryPressure(size); s_table.Add(obj, new _Remover(size)); } class _Remover { readonly long _size; public _Remover(long size) { _size = size; } ~_Remover() { //print.it("removed " + _size); GC.RemoveMemoryPressure(_size); } } /// /// Provides a static for USER handles, created with this code: new("Au.User", 300, 3000);. /// /// /// OS refuses to create more USER objects than the process quota. Normally it is 10000, but can be min 200. It is set in registry. /// public static HandleCollector UserHandleCollector { get; } = new("Au.User", 300, 3000); /// /// Provides a static for GDI handles, created with this code: new("Au.GDI", 300, 3000);. /// /// /// OS refuses to create more GDI objects than the process quota. Normally it is 10000, but can be min 200. It is set in registry. /// public static HandleCollector GdiHandleCollector { get; } = new("Au.GDI", 300, 3000); //The above HandleCollectors are used by icon. //Tested, works well. But if threshold 100 or 200, GC become too frequent sometimes. //Default limit for USER and GDI objects is 10000, min 200. //Alternatively could use GC.AddMemoryPressure/GC.RemoveMemoryPressure. // If we add 40000 bytes/icon and GC starts working when pressure is 4 MB, then the number of icons is ~100 and GDI handles ~300. // Problem: the GC threshold for unmanaged memory pressure is unknown, undocumented and changing. // Test results: used to be ~100 KB (why so small?), but now in .NET5 4MB (3MB in 32-bit process). // For managed memory it is 2 MB. // Could instead allocate managed array of this size and keep in a field. // Info: System.Drawing.Icon objects loaded from file or stream keep icon memory in private readonly byte[]? _iconData. // That is why it triggers GC quite soon. But eg Clone doesn't clone that memory and it can be dangerous. //Don't care about icon memory size. } } ================================================ FILE: Au/Internal/GDI misc.cs ================================================ namespace Au.More; /// /// Helps to get and release screen DC with using. /// Uses API GetDC and ReleaseDC. /// struct ScreenDC_ : IDisposable { IntPtr _dc; public ScreenDC_() => _dc = Api.GetDC(default); public static implicit operator IntPtr(ScreenDC_ dc) => dc._dc; public void Dispose() { if (_dc != default) { Api.ReleaseDC(default, _dc); _dc = default; } } } /// /// Helps to get and release window DC with using. /// Uses API GetDC and ReleaseDC. /// struct WindowDC_ : IDisposable { IntPtr _dc; wnd _w; public WindowDC_(IntPtr dc, wnd w) { _dc = dc; _w = w; } public WindowDC_(wnd w) => _dc = Api.GetDC(_w = w); public static implicit operator IntPtr(WindowDC_ dc) => dc._dc; public bool Is0 => _dc == default; public void Dispose() { if (_dc != default) { Api.ReleaseDC(_w, _dc); _dc = default; } } } /// /// Helps to create and delete compatible DC (memory DC) with using. /// Uses API CreateCompatibleDC and DeleteDC. /// class MemoryDC_ : IDisposable { protected IntPtr _dc; /// /// Creates memory DC compatible with screen. /// public MemoryDC_() : this(default) { } public MemoryDC_(IntPtr dc) => _dc = Api.CreateCompatibleDC(dc); public static implicit operator IntPtr(MemoryDC_ dc) => dc._dc; public bool Is0 => _dc == default; public void Dispose() => Dispose(true); protected virtual void Dispose(bool disposing) { if (_dc != default) { Api.DeleteDC(_dc); _dc = default; } } } /// /// Memory DC with selected font. /// Can be used for font measurement. /// sealed class FontDC_ : MemoryDC_ { IntPtr _oldFont; /// /// Selects specified font. /// The Dispose method will select it out but will not destroy it. /// /// public FontDC_(IntPtr font) { _oldFont = Api.SelectObject(_dc, font); } /// /// Selects standard UI font for specified DPI. /// /// public FontDC_(DpiOf dpi) : this(NativeFont_.RegularCached(dpi)) { } protected override void Dispose(bool disposing) { if (_oldFont != default) { Api.SelectObject(_dc, _oldFont); _oldFont = default; } base.Dispose(disposing); } /// /// Measures text with API GetTextExtentPoint32. /// Should be single line without tabs. For drawing with API TextOut or ExtTextOut. /// public SIZE MeasureEP(string s) { Api.GetTextExtentPoint32(_dc, s, s.Length, out var z); return z; } /// /// Measures text with API DrawTextEx. /// Can be multiline. For drawing with API DrawTextEx. /// public SIZE MeasureDT(RStr s, TFFlags format, int wrapWidth = 0) { if (s.Length == 0) return default; RECT r = new(0, 0, wrapWidth, 0); Api.DrawText(_dc, s, ref r, format | TFFlags.CALCRECT); return new(r.Width + 1, r.Height); //When drawing, may cut 1 pixel at the right, eg if text ends with T. Workaround: now add 1. } } struct GdiObject_ : IDisposable { IntPtr _h; public IntPtr Handle => _h; public GdiObject_(IntPtr handle) { _h = handle; } public void Dispose() { Api.DeleteObject(_h); _h = default; } public static implicit operator IntPtr(GdiObject_ g) => g._h; /// /// Calls API CreateSolidBrush. /// /// 0xRRGGBB. public static GdiObject_ ColorBrush(ColorInt color) { return new(Api.CreateSolidBrush(color.ToBGR())); } /// /// Calls API GetThemeSysColorBrush. /// /// Theme handle. If default, gets non-themed color. /// API COLOR_x. public static GdiObject_ SysColorBrush(IntPtr hTheme, int colorIndex) { return new(Api.GetThemeSysColorBrush(hTheme, colorIndex)); } public void BrushFill(IntPtr dc, RECT r) { Api.FillRect(dc, r, _h); } public void BrushRect(IntPtr dc, RECT r) { Api.FrameRect(dc, r, _h); } } struct GdiPen_ : IDisposable { IntPtr _pen; public IntPtr Handle => _pen; public GdiPen_(int color, int width = 1, int style = 0) { _pen = Api.CreatePen(style, width, color); } public void Dispose() { Api.DeleteObject(_pen); _pen = default; } /// /// Draws line and returns previous "current position". /// Don't need to select pen into DC. /// public POINT DrawLine(IntPtr dc, POINT start, POINT end) { var old = Api.SelectObject(dc, _pen); //fast Api.MoveToEx(dc, start.x, start.y, out var p); //fast Api.LineTo(dc, end.x, end.y); Api.SelectObject(dc, old); //fast return p; } } struct GdiSelectObject_ : IDisposable { IntPtr _dc, _old; public GdiSelectObject_(IntPtr dc, IntPtr obj) { _old = Api.SelectObject(_dc = dc, obj); } public void Dispose() { Api.SelectObject(_dc, _old); _dc = default; } } ================================================ FILE: Au/Internal/Handle_.cs ================================================ using Microsoft.Win32.SafeHandles; namespace Au.More; /// /// Manages a kernel handle. /// Must be disposed. /// Has static functions to open process handle. /// internal struct Handle_ : IDisposable { IntPtr _h; /// /// Attaches a kernel handle to this new variable. /// No exception when handle is invalid. /// If handle == -1, sets 0. /// /// public Handle_(nint handle) { _h = handle == -1 ? default : handle; } //public static explicit operator Handle_(IntPtr p) => new Handle_(p); //no public static implicit operator IntPtr(Handle_ p) => p._h; /// /// _h == default. /// Info: _h never is -1. /// public bool Is0 => _h == default; /// /// if (!Is0) { Api.CloseHandle(_h); _h = default; } /// public void Dispose() { if (!Is0) { Api.CloseHandle(_h); _h = default; } } /// /// Opens process handle. /// Calls API OpenProcess. /// /// default if failed. Supports . /// Process id. /// Desired access (Api.PROCESS_), as in API OpenProcess documentation. public static Handle_ OpenProcess(int processId, uint desiredAccess = Api.PROCESS_QUERY_LIMITED_INFORMATION) { if (processId == 0) { lastError.code = Api.ERROR_INVALID_PARAMETER; return default; } return _OpenProcess(processId, desiredAccess); } /// /// Opens window's process handle. /// This overload is more powerful: if API OpenProcess fails, it tries API GetProcessHandleFromHwnd, which can open higher integrity level processes, but only if current process is uiAccess and desiredAccess includes only PROCESS_DUP_HANDLE, PROCESS_VM_OPERATION, PROCESS_VM_READ, PROCESS_VM_WRITE, SYNCHRONIZE. /// /// default if failed. Supports . /// /// Desired access (Api.PROCESS_), as in API OpenProcess documentation. public static Handle_ OpenProcess(wnd w, uint desiredAccess = Api.PROCESS_QUERY_LIMITED_INFORMATION) { int pid = w.ProcessId; if (pid == 0) return default; return _OpenProcess(pid, desiredAccess, w); } static Handle_ _OpenProcess(int processId, uint desiredAccess = Api.PROCESS_QUERY_LIMITED_INFORMATION, wnd processWindow = default) { Handle_ R = Api.OpenProcess(desiredAccess, false, processId); if (R.Is0 && !processWindow.Is0 && 0 == (desiredAccess & ~(Api.PROCESS_DUP_HANDLE | Api.PROCESS_VM_OPERATION | Api.PROCESS_VM_READ | Api.PROCESS_VM_WRITE | Api.SYNCHRONIZE))) { int e = lastError.code; if (uacInfo.ofThisProcess.IsUIAccess) R = Api.GetProcessHandleFromHwnd(processWindow); if (R.Is0) Api.SetLastError(e); } return R; } } /// /// Kernel handle that is derived from WaitHandle. /// When don't need to wait, use , it's more lightweight and has more creation methods. /// internal class WaitHandle_ : WaitHandle { public WaitHandle_(IntPtr nativeHandle, bool ownsHandle) { base.SafeWaitHandle = new SafeWaitHandle(nativeHandle, ownsHandle); } /// /// Opens process handle. /// Returns null if failed. /// /// /// public static WaitHandle_ FromProcessId(int pid, uint desiredAccess) { try { var hp = Handle_.OpenProcess(pid, desiredAccess); if (!hp.Is0) return new WaitHandle_(hp, true); } catch (Exception ex) { Debug_.Print(ex); } return null; } } ================================================ FILE: Au/Internal/ILReader.cs ================================================ //https://stackoverflow.com/questions/14243284/how-can-i-retrieve-string-literals-using-reflection //#define FULL using System.Reflection.Emit; namespace Au.More; [StructLayout(LayoutKind.Sequential)] internal struct ILInstruction { private readonly OpCode operationCode; // 40. 56-64. The entire structure is very big. maybe do array lookup for opcode instead. private readonly byte[] instructionRawData; private readonly object instructionData; private readonly int instructionAddress; private readonly int index; internal ILInstruction(OpCode code, byte[] instructionRawData, int instructionAddress, object instructionData, int index) { this.operationCode = code; this.instructionRawData = instructionRawData; this.instructionAddress = instructionAddress; this.instructionData = instructionData; this.index = index; } public OpCode Op { get { return this.operationCode; } } /// /// Gets the raw data. /// public byte[] RawData { get { return this.instructionRawData; } } /// /// Gets the data. /// public object Data { get { return this.instructionData; } } /// /// Gets the address of the instruction. /// public int Address { get { return this.instructionAddress; } } /// /// Gets the index of the instruction. /// /// /// The index of the instruction. /// public int InstructionIndex { get { return this.index; } } /// /// Gets the value as integer /// /// The data value. public int DataValue { get { int value = 0; if (this.Data != null) { if (this.Data is byte) { value = (byte)this.Data; } else if (this.Data is short) { value = (short)this.Data; } else if (this.Data is int) { value = (int)this.Data; } } return value; } } /// /// Gets the length of the instructions and operands. /// /// The length. public int Length { get { return this.Op.Size + (this.RawData == null ? 0 : this.RawData.Length); } } #if DEBUG /// /// Returns a that represents this instance. /// /// /// A that represents this instance. /// public override string ToString() { StringBuilder builder = new StringBuilder(); builder.AppendFormat("0x{0:x4} {1,-10}", this.Address, this.Op.Name); if (this.Data != null) { builder.Append(this.Data.ToString()); } if (this.RawData != null && this.RawData.Length > 0) { builder.Append(" [0x"); for (int i = this.RawData.Length; --i >= 0;) { builder.Append(this.RawData[i].ToString("x2", System.Globalization.CultureInfo.InvariantCulture)); } builder.Append(']'); } return builder.ToString(); } #endif } /// /// Reads IL instructions from a byte stream. /// /// Allows generated code to be viewed without debugger or enabled debug assemblies. internal sealed class ILReader { /// /// The _instruction lookup. /// private static readonly Lazy> instructionLookup = new Lazy>(ILReader.GetLookupTable, System.Threading.LazyThreadSafetyMode.ExecutionAndPublication); /// /// The IL reader provider. /// private IILReaderProvider intermediateLanguageProvider; /// /// Initializes a new instance of the class. /// public ILReader(MethodInfo method, byte[] il = null) { //au: il added because the caller itself gets IL array and does nothing if it's too big to disassemble. To avoid getting IL 2 times. if (method == null) { throw new ArgumentNullException("method"); } this.intermediateLanguageProvider = ILReader.CreateILReaderProvider(method, il); } /// /// Gets the instructions. /// /// The instructions. public IEnumerable Instructions { get { byte[] instructionBytes = this.intermediateLanguageProvider.GetMethodBody(); int instructionIndex = 0, startAddress; for (int position = 0; position < instructionBytes.Length;) { startAddress = position; short operationData = instructionBytes[position]; if (IsInstructionPrefix(operationData)) { operationData = (short)((operationData << 8) | instructionBytes[++position]); } position++; OpCode code; if (!instructionLookup.Value.TryGetValue(operationData, out code)) { throw new InvalidProgramException(string.Format("0x{0:X2} is not a valid op code.", operationData)); } int dataSize = GetSize(code.OperandType); byte[] data = new byte[dataSize]; Buffer.BlockCopy(instructionBytes, position, data, 0, dataSize); object objData = this.GetData(code, data); position += dataSize; if (code.OperandType == OperandType.InlineSwitch) { dataSize = (int)objData; int[] labels = new int[dataSize]; for (int index = 0; index < labels.Length; index++) { labels[index] = BitConverter.ToInt32(instructionBytes, position); position += 4; } objData = labels; } yield return new ILInstruction(code, data, startAddress, objData, instructionIndex); instructionIndex++; } } } private static IILReaderProvider CreateILReaderProvider(MethodInfo methodInfo, byte[] il = null) { #if FULL IILReaderProvider reader = DynamicILReaderProvider.Create(methodInfo, il); if(reader != null) { return reader; } #endif return new ILReaderProvider(methodInfo, il); } /// /// Checks to see if the IL instruction is a prefix indicating the length of the instruction is two bytes long. /// /// The IL instruction as a byte. /// IL instructions can either be 1 or 2 bytes. /// True if this IL instruction is a prefix indicating the instruction is two bytes long. private static bool IsInstructionPrefix(short value) { return ((value & OpCodes.Prefix1.Value) == OpCodes.Prefix1.Value) || ((value & OpCodes.Prefix2.Value) == OpCodes.Prefix2.Value) || ((value & OpCodes.Prefix3.Value) == OpCodes.Prefix3.Value) || ((value & OpCodes.Prefix4.Value) == OpCodes.Prefix4.Value) || ((value & OpCodes.Prefix5.Value) == OpCodes.Prefix5.Value) || ((value & OpCodes.Prefix6.Value) == OpCodes.Prefix6.Value) || ((value & OpCodes.Prefix7.Value) == OpCodes.Prefix7.Value) || ((value & OpCodes.Prefixref.Value) == OpCodes.Prefixref.Value); } /// /// The get lookup table. /// /// /// A dictionary of IL instructions. /// private static Dictionary GetLookupTable() { // Might be better to do an array lookup. Use a seperate arrary for instructions without a prefix and array for each prefix. Dictionary lookupTable = new Dictionary(); FieldInfo[] fields = typeof(OpCodes).GetFields(BindingFlags.Static | BindingFlags.Public); foreach (FieldInfo field in fields) { OpCode code = (OpCode)field.GetValue(null); lookupTable.Add(code.Value, code); } return lookupTable; } /// /// Gets the size of a operand. /// /// Defines the type of operand. /// The size in bytes of the operand type. private static int GetSize(OperandType operandType) { switch (operandType) { case OperandType.InlineNone: return 0; case OperandType.ShortInlineBrTarget: case OperandType.ShortInlineI: case OperandType.ShortInlineVar: return 1; case OperandType.InlineVar: return 2; case OperandType.InlineBrTarget: case OperandType.InlineField: case OperandType.InlineI: case OperandType.InlineMethod: case OperandType.InlineSig: case OperandType.InlineString: case OperandType.InlineSwitch: case OperandType.InlineTok: case OperandType.InlineType: case OperandType.ShortInlineR: return 4; case OperandType.InlineI8: case OperandType.InlineR: return 8; default: return 0; } } private object GetData(OpCode code, byte[] rawData) { object data = null; switch (code.OperandType) { case OperandType.InlineField: data = this.intermediateLanguageProvider.ResolveField(BitConverter.ToInt32(rawData, 0)); break; case OperandType.InlineSwitch: data = BitConverter.ToInt32(rawData, 0); break; case OperandType.InlineBrTarget: case OperandType.InlineI: data = BitConverter.ToInt32(rawData, 0); break; case OperandType.InlineI8: data = BitConverter.ToInt64(rawData, 0); break; case OperandType.InlineMethod: data = this.intermediateLanguageProvider.ResolveMethod(BitConverter.ToInt32(rawData, 0)); break; case OperandType.InlineR: data = BitConverter.ToDouble(rawData, 0); break; case OperandType.InlineSig: data = this.intermediateLanguageProvider.ResolveSignature(BitConverter.ToInt32(rawData, 0)); break; case OperandType.InlineString: data = this.intermediateLanguageProvider.ResolveString(BitConverter.ToInt32(rawData, 0)); break; case OperandType.InlineTok: case OperandType.InlineType: data = this.intermediateLanguageProvider.ResolveType(BitConverter.ToInt32(rawData, 0)); break; case OperandType.InlineVar: data = BitConverter.ToInt16(rawData, 0); break; case OperandType.ShortInlineVar: case OperandType.ShortInlineI: case OperandType.ShortInlineBrTarget: data = rawData[0]; break; case OperandType.ShortInlineR: data = BitConverter.ToSingle(rawData, 0); break; } return data; } interface IILReaderProvider { byte[] GetMethodBody(); FieldInfo ResolveField(int metadataToken); MemberInfo ResolveMember(int metadataToken); MethodBase ResolveMethod(int metadataToken); byte[] ResolveSignature(int metadataToken); string ResolveString(int metadataToken); Type ResolveType(int metadataToken); } class ILReaderProvider : IILReaderProvider { byte[] _il; public ILReaderProvider(MethodInfo method, byte[] il = null) { _il = il; this.Method = method; this.MethodBody = method.GetMethodBody(); this.MethodModule = method.Module; } public MethodInfo Method { get; private set; } public MethodBody MethodBody { get; private set; } public Module MethodModule { get; private set; } public byte[] GetMethodBody() { return _il ??= this.MethodBody.GetILAsByteArray(); } public FieldInfo ResolveField(int metadataToken) { return this.MethodModule.ResolveField(metadataToken); } public MemberInfo ResolveMember(int metadataToken) { return this.MethodModule.ResolveMember(metadataToken); } public MethodBase ResolveMethod(int metadataToken) { return this.MethodModule.ResolveMethod(metadataToken); } public byte[] ResolveSignature(int metadataToken) { return this.MethodModule.ResolveSignature(metadataToken); } public string ResolveString(int metadataToken) { return this.MethodModule.ResolveString(metadataToken); } public Type ResolveType(int metadataToken) { return this.MethodModule.ResolveType(metadataToken); } } #if FULL class DynamicILReaderProvider :IILReaderProvider { public const int TypeRidPrefix = 0x02000000; public const int MethodRidPrefix = 0x06000000; public const int FieldRidPrefix = 0x04000000; public static readonly Type RuntimeDynamicMethodType; private static readonly FieldInfo fileLengthField = typeof(ILGenerator).GetField("m_length", BindingFlags.NonPublic | BindingFlags.Instance); private static readonly FieldInfo IntermediateLanguageBytesField = typeof(ILGenerator).GetField("m_ILStream", BindingFlags.NonPublic | BindingFlags.Instance); private static readonly MethodInfo bakeByteArrayMethod = typeof(ILGenerator).GetMethod("BakeByteArray", BindingFlags.NonPublic | BindingFlags.Instance); private static readonly PropertyInfo dynamicScopeIndexor; private static readonly FieldInfo dynamicScopeField; private static readonly Type genericMethodInfoType; private static readonly FieldInfo genericMethodHandleField; private static readonly FieldInfo genericMethodContextField; private static readonly Type varArgMethodType; private static readonly FieldInfo varArgMethodMethod; private static readonly Type genericFieldInfoType; private static readonly FieldInfo genericFieldInfoHandle; private static readonly FieldInfo genericFieldInfoContext; private static readonly FieldInfo ownerField; private object dynamicScope; private ILGenerator generator; static DynamicILReaderProvider() { BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Instance; dynamicScopeIndexor = Type.GetType("System.Reflection.Emit.DynamicScope").GetProperty("Item", bindingFlags); dynamicScopeField = Type.GetType("System.Reflection.Emit.DynamicILGenerator").GetField("m_scope", bindingFlags); varArgMethodType = Type.GetType("System.Reflection.Emit.VarArgMethod"); varArgMethodMethod = varArgMethodType.GetField("m_method", bindingFlags); genericMethodInfoType = Type.GetType("System.Reflection.Emit.GenericMethodInfo"); genericMethodHandleField = genericMethodInfoType.GetField("m_methodHandle", bindingFlags); genericMethodContextField = genericMethodInfoType.GetField("m_context", bindingFlags); genericFieldInfoType = Type.GetType("System.Reflection.Emit.GenericFieldInfo", false); if(genericFieldInfoType != null) { genericFieldInfoHandle = genericFieldInfoType.GetField("m_fieldHandle", bindingFlags); genericFieldInfoContext = genericFieldInfoType.GetField("m_context", bindingFlags); } else { genericFieldInfoHandle = genericFieldInfoContext = null; } RuntimeDynamicMethodType = typeof(DynamicMethod).GetNestedType("RTDynamicMethod", BindingFlags.NonPublic); ownerField = RuntimeDynamicMethodType.GetField("m_owner", bindingFlags); } private DynamicILReaderProvider(DynamicMethod method) { this.Method = method; this.generator = method.GetILGenerator(); this.dynamicScope = dynamicScopeField.GetValue(this.generator); } public DynamicMethod Method { get; private set; } internal object this[int token] { get { return dynamicScopeIndexor.GetValue(this.dynamicScope, new object[] { token }); } } public static DynamicILReaderProvider Create(MethodInfo method) { if(method == null) { throw new ArgumentNullException("method"); } DynamicMethod dynamicMethod = method as DynamicMethod; if(dynamicMethod != null) { return new DynamicILReaderProvider(dynamicMethod); } Type methodType = method.GetType(); if(RuntimeDynamicMethodType.IsAssignableFrom(methodType)) { return new DynamicILReaderProvider(ownerField.GetValue(method) as DynamicMethod); } return null; } public byte[] GetMethodBody() { byte[] data = null; ILGenerator ilgen = this.Method.GetILGenerator(); try { data = (byte[])bakeByteArrayMethod.Invoke(ilgen, null) ?? new byte[0]; } catch(TargetInvocationException) { int length = (int)fileLengthField.GetValue(ilgen); data = new byte[length]; Array.Copy((byte[])IntermediateLanguageBytesField.GetValue(ilgen), data, length); } return data; } public FieldInfo ResolveField(int metadataToken) { object tokenValue = this[metadataToken]; if(tokenValue is RuntimeFieldHandle) { return FieldInfo.GetFieldFromHandle((RuntimeFieldHandle)tokenValue); } if(tokenValue.GetType() == DynamicILReaderProvider.genericFieldInfoType) { return FieldInfo.GetFieldFromHandle( (RuntimeFieldHandle)genericFieldInfoHandle.GetValue(tokenValue), (RuntimeTypeHandle)genericFieldInfoContext.GetValue(tokenValue)); } return null; } public MemberInfo ResolveMember(int metadataToken) { if((metadataToken & TypeRidPrefix) != 0) { return this.ResolveType(metadataToken); } if((metadataToken & MethodRidPrefix) != 0) { return this.ResolveMethod(metadataToken); } if((metadataToken & FieldRidPrefix) != 0) { return this.ResolveField(metadataToken); } return null; } public MethodBase ResolveMethod(int metadataToken) { object tokenValue = this[metadataToken]; DynamicMethod dynamicMethod = tokenValue as DynamicMethod; if(dynamicMethod != null) { return dynamicMethod; } if(tokenValue is RuntimeMethodHandle) { return MethodBase.GetMethodFromHandle((RuntimeMethodHandle)this[metadataToken]); } if(tokenValue.GetType() == DynamicILReaderProvider.genericFieldInfoType) { return MethodBase.GetMethodFromHandle( (RuntimeMethodHandle)genericMethodHandleField.GetValue(tokenValue), (RuntimeTypeHandle)genericMethodContextField.GetValue(tokenValue)); } if(tokenValue.GetType() == DynamicILReaderProvider.varArgMethodType) { return DynamicILReaderProvider.varArgMethodMethod.GetValue(tokenValue) as MethodInfo; } return null; } public byte[] ResolveSignature(int metadataToken) { return this[metadataToken] as byte[]; } public string ResolveString(int metadataToken) { return this[metadataToken] as string; } public Type ResolveType(int metadataToken) { return Type.GetTypeFromHandle((RuntimeTypeHandle)this[metadataToken]); } } #endif } ================================================ FILE: Au/Internal/IconString_.cs ================================================ using System.Windows; using System.Windows.Media; namespace Au.More; /// /// Parses XAML icon strings like "*Pack.Name color". /// record struct IconString_ { public readonly string pack, name, color; public readonly int size, end; public readonly Thickness margin; public readonly char stretch; public readonly bool snapPixels; public bool HasValue => name != null; public bool HasSize => size is > 0 and <= 16; public bool HasMargin => margin != default; IconString_(RStr s) { if (!Detect(s, out var d)) return; pack = s[d.pack.Range].ToString(); name = s[d.name.Range].ToString(); int k = d.name.end; end = s.IndexOf(k, ';'); if (end < 0) end = s.Length; if (s.Eq(k, ' ')) { s = s[++k..end]; Span a1 = stackalloc Range[3], a2 = stackalloc Range[6]; foreach (var v in a1[..s.Split(a1, ' ')]) { int i = v.Start.Value, j = v.End.Value; if (j == i) continue; var c = s[i++]; if (c is '#' or '|' || c.IsAsciiAlpha()) color = s[v].ToString(); else if (c == '@') size = s[i..].ToInt_(); else if (c == '%') { //margin int n = 0; var m = s[i..j]; foreach (var u in a2[..m.Split(a2, ',')]) { n++; if (u.End.Value == u.Start.Value) continue; if (n == 6) { snapPixels = m[u][0] == 'p'; } else if (n == 5) { stretch = m[u][0]; if (!(stretch is 'f' or 'm')) stretch = default; } else if (double.TryParse(m[u], CultureInfo.InvariantCulture, out var g)) { switch (n) { case 1: margin.Left = g; break; case 2: margin.Top = g; break; case 3: margin.Right = g; break; case 4: margin.Bottom = g; break; } } } } } } //note: don't use `MemoryExtensions.SpanSplitEnumerator RStr.Split(char separator)`. Easier to use, but: 1. Not in .NET 8. 2. 5-10 times slower. } /// /// Parses single-icon string. Format: "[*<library>]*pack.name[ color][ @size][ %margin]". /// /// false if bad format. public static bool Parse(RStr s, out IconString_ r) { r = new(s); return r.HasValue; } /// /// Parses single-or-multi-icon string. Format: "[*<library>]*pack.name[ color][ @size][ %margin][;more icons]". /// /// null if bad format. public static IconString_[] ParseAll(string icon) { List a = null; for (RStr s = icon; Parse(s, out var r); s = s[(r.end + 1)..].TrimStart()) { (a ??= new()).Add(r); if (r.end == s.Length || s[r.end] != ';') break; } return a?.ToArray(); } /// /// Returns true if s starts with "*pack.name" or "*<library>*pack.name". /// public static bool Detect(RStr s, out (StartEnd pack, StartEnd name) r) { r = default; if (s.Length < 8 || s[0] != '*' || !s.Contains('.')) return false; Span a = stackalloc StartEnd[3]; if (!s_rxDetect.Match(s, a)) return false; r = (a[1], a[2]); return true; } static regexp s_rxDetect = new regexp(@"^\*(?:<.+?>\*)?([[:alpha:]]\w+)\.(\w\w+)"); /// /// Our compiler (_CreateManagedResources) adds XAML of icons to resources from literal strings. This function gets the XAML. /// /// /// null if failed. public static string GetXamlFromResources(string icon) { //print.it("\r\n----", icon); if (DetectAndRemoveParametersForResources(icon, out string icon2) && ResourceUtil.TryGetString_(icon2) is string xaml) { //print.it(xaml); var a = ParseAll(icon); if (!XamlSetColorSizeMargin(ref xaml, a)) return null; //print.it(xaml); return xaml; } else { Debug_.Print(icon); return null; } } static regexp s_rxPath = new regexp(@""); static regexp s_rxAttr = new(@" (?:Fill|Stroke|Width|Height|Margin|Stretch|SnapsToDevicePixels)=""[^""]*"); /// /// In XAML sets color, size and margin specified in parsed icons. /// /// XAML with as many Path as a elements. /// Parsed icons. /// false if found errors etc. public static bool XamlSetColorSizeMargin(ref string xaml, IconString_[] a) { using var b_ = new StringBuilder_(out var b, xaml.Length / 10 * 11 + 100); int appendFrom = 0, i = 0; bool anyHasSizeOrMargin = a.Any(o => o.HasSize || o.HasMargin); foreach (var gp in s_rxPath.FindAllG(xaml, 1)) { //for each if (i == a.Length) return false; var x = a[i++]; bool hasSize = x.HasSize, hasMargin = x.HasMargin, xamlHasSizeOrMargin = false; foreach (var ga in s_rxAttr.FindAllG(xaml, 0, gp.Start..gp.End)) { //for each attribute int attr = ga.Start + 1; if (xaml[attr] is 'F' or 'S') { string sVal; if (xaml[attr + 1] == 'n') { //SnapsToDevicePixels if (hasSize || x.snapPixels) sVal = "True"; else continue; } else if (xaml[attr + 3] == 'e') { //Stretch if (x.stretch is 'f' or 'm') sVal = x.stretch is 'f' ? "Fill" : "UniformToFill"; else continue; } else { sVal = NormalizeColor(x.color); } b.Append(xaml, appendFrom, xaml.IndexOf('"', attr) + 1 - appendFrom).Append(sVal); appendFrom = ga.End; } else { xamlHasSizeOrMargin = true; if (hasSize || hasMargin) { print.warning($"@size and %margin not supported for this icon: *{x.pack}.{x.name}", -1); return false; } } } if (hasSize || hasMargin || (anyHasSizeOrMargin && !xamlHasSizeOrMargin)) { b.Append(xaml, appendFrom, gp.End - appendFrom); appendFrom = gp.End; if (hasSize) { if (hasMargin) { print.warning($"Error in icon *{x.pack}.{x.name}: use @size or %margin, not both.", -1); return false; } b.AppendFormat(" Width=\"{0}\" Height=\"{0}\" Margin=\"{1}\"", x.size, ((16 - x.size) / 2.0).ToS()); } else if (hasMargin) { double wid = 16 - x.margin.Left - x.margin.Right, hei = 16 - x.margin.Top - x.margin.Bottom; if (wid <= 0 || hei <= 0) { print.warning($"Error in icon *{x.pack}.{x.name}: margins left+right and top+bottom must be < 16.", -1); return false; } b.AppendFormat(" Width=\"{0}\" Height=\"{1}\" Margin=\"{2}\"", wid.ToS(), hei.ToS(), x.margin.ToString()); } else { //if like "icon1;icon2", and some icons have specified size or margin, and some others don't, some icons may be rendered very small, depending on others. // Don't know why. Workaround: specify Width/Height for all. b.Append(" Width=\"16\" Height=\"16\""); } } } b.Append(xaml, appendFrom, xaml.Length - appendFrom); xaml = b.ToString(); return true; } /// /// If s is or contains "color|color2", removes "|color2" or "color|" depending on . /// If then (or initially) s is empty, returns SystemColors.ControlTextColor. /// public static string NormalizeColor(string s) { int i = s?.IndexOf('|') ?? -1; if (i >= 0) { bool dark = WpfUtil_.IsHighContrastDark; using (new StringBuilder_(out var b)) { int appendFrom = 0; do { int j = i + 1; if (dark) { while (i > 0 && s[i - 1] != ' ') i--; } else { while (j < s.Length && !(s[j] is ' ' or ';')) j++; } b.Append(s, appendFrom, i - appendFrom); i = s.IndexOf('|', j); appendFrom = j; } while (i > 0); b.Append(s, appendFrom, s.Length - appendFrom); s = b.ToString(); } } if (s.NE()) s = SystemColors.ControlTextColor.ToString(); return s; } /// /// If s is an icon string, removes parameters and returns true. ///
Example1: "*Pack.Name color" -> "*Pack.Name". ///
Example2: *Pack.Name1 color; *Pack.Name2 color margin;"]]> -> "*Pack.Name1;Pack.Name2". ///
public static bool DetectAndRemoveParametersForResources(RStr s, out string result) { StringBuilder b = null; while (Detect(s, out var d)) { if (b == null) b = new(); else b.Append(';'); b.Append('*').Append(s[d.pack.Range]).Append('.').Append(s[d.name.Range]); int semi = s.IndexOf(d.name.end, ';'); if (semi < 0) break; s = s[++semi..].TrimStart(); } return (result = b?.ToString()) != null; } } ================================================ FILE: Au/Internal/Jit_.cs ================================================ namespace Au.More; /// /// JIT-compiles methods. /// static class Jit_ { const BindingFlags c_bindingFlags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly; /// /// JIT-compiles method. /// Uses . /// /// Type containing the method. /// Method name. /// Method does not exist. /// Multiple overloads exist. public static void Compile(Type type, string method) { var m = type.GetMethod(method, c_bindingFlags); if (m == null) throw new ArgumentException($"Method {type.Name}.{method} does not exist."); RuntimeHelpers.PrepareMethod(m.MethodHandle); //tested: maybe MethodHandle.GetFunctionPointer can be used to detect whether the method is jited and assembly ngened. // Call GetFunctionPointer before and after PrepareMethod. If was not jited, the second call returns a different value. // Undocumented, therefore unreliable. } //rejected. Don't JIT-compile overloads. ///// ///// JIT-compiles a method overload. ///// Uses RuntimeHelpers.PrepareMethod. ///// ///// Type containing the method. ///// Method name. ///// Types of parameters of this overload. ///// Method does not exist. ///// Multiple overloads exist that match paramTypes. //public static void Compile(Type type, string method, params Type[] paramTypes) //{ // var m = type.GetMethod(method, c_bindingFlags, null, paramTypes, null); // if(m == null) throw new ArgumentException($"Method {type.Name}.{method} does not exist."); // RuntimeHelpers.PrepareMethod(m.MethodHandle); // //tested: MethodHandle.GetFunctionPointer cannot be used to detect whether the method is jited. // // Tried to find a faster way to detect whether the assembly is ngened. //} /// /// JIT-compiles multiple methods of same type. /// Uses . /// /// Type containing the methods. /// Method names. /// Method does not exist. public static void Compile(Type type, params string[] methods) { foreach (var v in methods) Compile(type, v); } } ================================================ FILE: Au/Internal/LaDebugger_.cs ================================================ namespace Au.More; class LaDebugger_ { //Called by netcoredbg as a breakpoint condition when specified Message. static bool Logpoint(bool condition, string s, string link) { if (condition) print.it($"<>♦<><> {s}<>"); return false; } //Same problems as with LaDebugger_. Also fails if struct. //static string Print(T t) { // var s = print.util.toString(t).Limit(1_000_000); // return Convert.ToBase64String(Encoding.UTF8.GetBytes(s)); //} } //Functions called by netcoredbg -var-create to get a value in the print.it format. //netcoredbg does not support cast to object etc. This is the only way that works. //Fails if array. //Fails if ref struct. //Garbage if object or dynamic. class LaDebugger_ { static string Print(T t) => _Print(t, false); static string PrintCompact(T t) => _Print(t, true); static string _Print(T t, bool compact) { var s = print.util.toString(t, compact).Limit(1_000_000); return Convert.ToBase64String(Encoding.UTF8.GetBytes(s)); } //static string PrintArray(T[] a) => "OK"; //fails too } ================================================ FILE: Au/Internal/LineWriter_.cs ================================================ namespace Au.More; /// /// optimized for writing full lines. /// /// /// Derived class must override . Don't need to override Write/WriteLine. /// If Write called with text that does not end with '\n', just accumulates the text in this variable. /// When called WriteLine or Flush or Write with text that ends with '\n', calls of the derived class. /// internal abstract class LineWriter_ : TextWriter { StringBuilder _b; /// /// Returns Encoding.Unicode. /// public override Encoding Encoding => Encoding.Unicode; /// /// If value is '\n', writes accumulated text as full line and clears accumulated text, else just appends value to the accumulated text. /// public override void Write(char value) { //qm2.write((int)value, value); if (value == '\n') { WriteLine(); } else { (_b ??= new StringBuilder()).Append(value); } base.Write(value); } /// /// If value ends with '\n', writes line (accumulated text + value) and clears accumulated text, else just appends value to the accumulated text. /// public override void Write(string value) { //qm2.write($"'{value}'"); //qm2.write("Write", $"'{value}'", value.ToCharArray()); if (value.NE()) return; if (value.Ends('\n')) { WriteLine(value[..^(value.Ends("\r\n") ? 2 : 1)]); } else { (_b ??= new StringBuilder()).Append(value); } } /// /// If this variable contains accumulated text, writes it as full line and clears it. Else writes empty line. /// public override void WriteLine() { WriteLineNow(_PrependBuilder(null)); } /// /// Writes line (accumulated text + value) and clears accumulated text. /// public override void WriteLine(string value) { //qm2.write("WriteLine", $"'{value}'", value.ToCharArray()); WriteLineNow(_PrependBuilder(value)); } string _PrependBuilder(string value) { if (_b != null && _b.Length > 0) { value = _b.ToString() + value; _b.Clear(); } return value; } /// /// If this variable contains accumulated text, writes it as full line and clears it. /// public override void Flush() { var s = _PrependBuilder(null); if (!s.NE()) WriteLineNow(s); } /// /// Called to write full line. /// /// Line text. Does not end with line break characters. protected abstract void WriteLineNow(string s); } ================================================ FILE: Au/Internal/MiniProgram_.cs ================================================ //PROBLEM: slow startup. //A minimal script starts in 70-100 ms cold, 40 hot (old PC, tested long time ago). Now on new PC starts in 37 ms. //Workaround for role miniProgram: // Preload task process. Let it wait for next task. While waiting, it also can JIT etc. // Then starts in 12/4 ms (cold/hot). With script.setup 15/5. That was an old test, now should be slower. Now on new PC 10/6 and 12/8. // Except first time. Also not faster if several scripts are started without a delay. Never mind. // This is implemented in this class and in Au.AppHost (just ~10 code lines added in 1 place). //Not using this workaround since v1.1, unless meta startFaster true (undocumented). Because: // Sometimes something does not work well (see problems below). // Since this was invented, .NET process startup became faster (faster JIT etc). Also computers faster. The delay now is barely noticeable. //FUTURE: if this workaround is sometimes useful, make meta startFaster public. Else remove the preloading code. //PROBLEM when preloaded: miniProgram windows start inactive, behind one or more windows. Unless they activate self, like dialog. // It does not depend on the foreground lock setting/API. The setting/API just enable SetForegroundWindow, but most windows don't call it. // Workaround: use CBT hook. It receives HCBT_ACTIVATE even when the window does not become the foreground window. // On HCBT_ACTIVATE, async-call SetForegroundWindow. Also, editor calls AllowSetForegroundWindow before starting task. //PROBLEM when preloaded: when miniProgram starts a console program, occasionally its window is inactive, although on top of other windows. // To reproduce: run miniProgram script: `run.it("cmd.exe", flags: RFlags.InheritAdmin)`. If starts active, wait at least 30 s and run again. // Never noticed it on Windows 10, only on Windows 11. // Workaround: after `run.it` wait a while, eg `1.s();`, because it happens only if this process exits immediately. //PROBLEM when preloaded: inherits old environment variables. //PROBLEM when preloaded: the preloaded processes may be confusing, even for me sometimes. //PROBLEM: although Main() starts fast, but the process ends slowly, because of .NET. // Eg if starting an empty script every <50 ms, sometimes cannot start. //Smaller problem: .NET creates many threads. No workaround. /* //To test task startup speed, use script "task startup speed.cs": 300.ms(); //give time to preload new task process for (int i = 0; i < 5; i++) { // perf.cpu(); var t=perf.ms.ToString(); script.run(@"miniProgram.cs", t); // script.run(@"exeProgram.cs", t); 600.ms(); //give time for the process to exit } //miniProgram.cs and exeProgram.cs: print.it(perf.ms-Int64.Parse(args[0])); */ using System.Runtime.Loader; namespace Au.More; /// /// Prepares to quickly start and execute a script with role miniProgram in this preloaded task process. Or starts/executes in this non-preloaded process. /// static unsafe class MiniProgram_ { struct _TaskInit { public IntPtr asmFile; public IntPtr* args; public int nArgs; } /// /// Called by apphost. /// [MethodImpl(MethodImplOptions.NoOptimization)] static void Init(nint pn, out _TaskInit r) { r = default; string pipeName = new((char*)pn); script.role = SRole.MiniProgram; process.ThisThreadSetComApartment_(ApartmentState.STA); //1.5 ms script.AppModuleInit_(auCompiler: true); //3 ms //rejected. Now this is implemented in editor. To detect when failed uses process exit code. Never mind exception text, it is not very useful. //process.thisProcessExit += e => { //0.9 ms // if (s_started != 0) print.TaskEvent_(e == null ? "TE" : "TF " + e.ToStringWithoutStack(), s_started); //}; #if true if (!Api.WaitNamedPipe(pipeName, -1)) return; #else //rejected: JIT some functions in other thread. Now everything much faster than with old .NET. // Speed of p1: with this 3500, without 6000 (slow Deserialize JIT). for (int i = 0; ; i++) { if (Api.WaitNamedPipe(pipeName, i == 1 ? -1 : 25)) break; if (Marshal.GetLastWin32Error() != Api.ERROR_SEM_TIMEOUT) return; if (i == 1) break; //rejected: ProfileOptimization. Now everything is JIT-ed and is as fast as can be. run.thread(() => { //using var p2 = perf.local(); //JIT Jit_.Compile(typeof(Serializer_), "Deserialize"); //tested: now Api functions fast, don't JIT. //p2.Next(); Jit_.Compile(typeof(script), nameof(script.setup), "_AuxThread"); //p2.Next(); //Thread.Sleep(20); //p2.Next(); //"Au".ToLowerInvariant(); //15-40 ms //now <1 ms //if need to preload some assemblies, use code like this. But now .NET loads assemblies fast, not like in old framework. //_ = typeof(TypeFromAssembly).Assembly; }, sta: false); } #endif //Debug_.PrintLoadedAssemblies(true, true); //using var p1 = perf.local(); using var pipe = Api.CreateFile(pipeName, Api.GENERIC_READ, 0, Api.OPEN_EXISTING, 0); if (pipe.Is0) { Debug_.PrintNativeError(); return; } //p1.Next(); int size; if (!Api.ReadFile(pipe, &size, 4, out int nr, default) || nr != 4) return; if (!Api.ReadFileArr(pipe, out var b, size, out nr) || nr != size) return; //p1.Next(); var a = Serializer_.Deserialize(b); //p1.Next('d'); var flags = (MPFlags)(int)a[2]; r.asmFile = Marshal.StringToCoTaskMemUTF8(a[1]); //p1.Next(); string[] args = a[3]; if (!args.NE_()) { r.nArgs = args.Length; r.args = (IntPtr*)Marshal.AllocHGlobal(args.Length * sizeof(IntPtr)); for (int i = 0; i < args.Length; i++) r.args[i] = Marshal.StringToCoTaskMemUTF8(args[i]); } //p1.Next(); script.s_idMainFile = (uint)(int)a[6]; script.s_wndEditorMsg = (wnd)(int)a[8]; script.s_wrPipeName = a[4]; if (0 != (flags & MPFlags.FromEditor)) script.testing = true; if (0 != (flags & MPFlags.IsPortable)) ScriptEditor.IsPortable = true; folders.Editor = new(folders.ThisApp); folders.Workspace = new(a[5]); if (0 != (flags & MPFlags.RefPaths)) AssemblyLoadContext.Default.Resolving += (alc, an) => ResolveAssemblyFromRefPathsAttribute_(alc, an, AssemblyUtil_.GetEntryAssembly()); if (0 != (flags & MPFlags.NativePaths)) AssemblyLoadContext.Default.ResolvingUnmanagedDll += (_, dll) => ResolveUnmanagedDllFromNativePathsAttribute_(dll, AssemblyUtil_.GetEntryAssembly()); if (0 != (flags & MPFlags.MTA)) process.ThisThreadSetComApartment_(ApartmentState.MTA); if (0 != (flags & MPFlags.Console)) { Api.AllocConsole(); } else { if (0 != (flags & MPFlags.RedirectConsole)) RedirectConsole_(); //Compiler adds this flag if the script uses System.Console assembly. //Else new users would not know how to test code examples with Console.WriteLine found on the internet. } //p1.Next(); script.Starting_(a[0], a[7], preloaded: 0 != (flags & MPFlags.Preloaded)); //Api.QueryPerformanceCounter(out s_started); //print.TaskEvent_("TS", s_started); } //for assemblies used in miniProgram and editorExtension scripts internal static Assembly ResolveAssemblyFromRefPathsAttribute_(AssemblyLoadContext alc, AssemblyName an, Assembly scriptAssembly) { //print.it("managed", an); //note: don't cache GetCustomAttribute/split results. It's many times faster than LoadFromAssemblyPath and JIT. var attr = scriptAssembly.GetCustomAttribute(); if (attr != null) { string name = an.Name; foreach (var v in attr.Paths.Split('|')) { //print.it(v); int iName = v.Length - name.Length - 4; if (iName <= 0 || v[iName - 1] != '\\' || !v.Eq(iName, name, true)) continue; if (!filesystem.exists(v).File) continue; return alc.LoadFromAssemblyPath_(v); } } return null; } internal static Assembly LoadFromAssemblyPath_(this AssemblyLoadContext t, string path) { try { return t.LoadFromAssemblyPath(path); } catch { } //catch (FileLoadException e1) { // Debug_.Print("alc.LoadFromAssemblyPath failed. Will retry with s_alc. " + e1); //} //If the assembly has the same name as one of TPA assemblies (probably it's a newer version), // the above LoadFromAssemblyPath ignores the path and tries to load the TPA assembly, and fails. // Workaround: Then try to load to another AssemblyLoadContext. //return Assembly.LoadFile(path); //works, but better use the same context for all s_alc ??= new("Resolving"); return s_alc.LoadFromAssemblyPath(path); } static AssemblyLoadContext s_alc; //for assemblies used in miniProgram and editorExtension scripts internal static IntPtr ResolveUnmanagedDllFromNativePathsAttribute_(string name, Assembly scriptAssembly) { var attr = scriptAssembly.GetCustomAttribute(); if (attr != null) { if (!name.Ends(".dll", true)) name += ".dll"; foreach (var v in attr.Paths.Split('|')) { //print.it(v); if (!v.Ends(name, true) || !v.Eq(v.Length - name.Length - 1, '\\')) continue; if (NativeLibrary.TryLoad(v, out var h)) return h; } } return default; } /// /// Used by exeProgram. /// /// Directory that may contain subdir "runtimes". internal static void ResolveNugetRuntimes_(string rootDir) { var runtimesDir = pathname.combine(rootDir, "runtimes"); if (!filesystem.exists(runtimesDir).Directory) return; //This code is similar as in Compiler._GetDllPaths:_AddGroup. There we get paths from XML, here from filesystem. int verPC = osVersion.minWin10 ? 100 : osVersion.minWin8_1 ? 81 : osVersion.minWin8 ? 80 : 70; //don't need Win11 var flags = FEFlags.AllDescendants | FEFlags.IgnoreInaccessible | FEFlags.NeedRelativePaths | FEFlags.UseRawPath; List<(FEFile f, int ver)> aNet = [], aNative = []; foreach (var f in filesystem.enumFiles(runtimesDir, "*.dll", flags)) { var s = f.Name; if (!s.Starts(@"\win", true) || s.Length < 10) continue; int i = 4, verDll = 0; if (s[i] is >= '0' and <= '9') { verDll = s.ToInt(i, out i); if (verDll != 81) verDll *= 10; if (verDll > verPC) continue; } if (s[i] == '-') { string arch = RuntimeInformation.ProcessArchitecture switch { Architecture.X86 => @"-x86\", Architecture.Arm64 => @"-arm64\", _ => @"-x64\" }; if (!s.Eq(i, arch, true)) continue; i += arch.Length; } else if (s[i++] != '\\') { continue; } var a = s.Eq(i, @"native\", true) ? aNative : aNet; a.Add((f, verDll)); } var dr = _Do(aNet); var dn = _Do(aNative); static Dictionary _Do(List<(FEFile f, int ver)> a) { if (a.Count == 0) return null; Dictionary d = null; foreach (var group in a.ToLookup(o => pathname.getNameNoExt(o.f.Name), StringComparer.OrdinalIgnoreCase)) { //print.it($"<>{group.Key}<>"); int verBest = -1; string sBest = null; foreach (var (f, verDll) in group) { if (verDll > verBest) { verBest = verDll; sBest = f.FullPath; } } if (sBest != null) { //print.it(sBest); d ??= new(StringComparer.OrdinalIgnoreCase); d[group.Key] = sBest; } } return d; } if (dr != null) AssemblyLoadContext.Default.Resolving += (alc, an) => { if (!dr.TryGetValue(an.Name, out var path)) return null; return alc.LoadFromAssemblyPath_(path); }; if (dn != null) AssemblyLoadContext.Default.ResolvingUnmanagedDll += (_, name) => { if (name.Ends(".dll", true)) name = name[..^4]; if (!dn.TryGetValue(name, out var path)) return default; if (!NativeLibrary.TryLoad(path, out var r)) return default; return r; }; } [Flags] public enum MPFlags { /// Has [RefPaths] attribute. It is when using meta r or nuget. RefPaths = 1, /// Main with [MTAThread]. MTA = 2, /// Has meta console true. Console = 4, /// Uses System.Console assembly. RedirectConsole = 8, /// Has [NativePaths] attribute. It is when using NuGet packages with native dlls. NativePaths = 16, /// Started from editor with the Run button or menu command. Used for . FromEditor = 32, /// Started from portable editor. IsPortable = 64, /// Using a preloaded process. Preloaded = 128, //Config = 256, //meta hasConfig } class _ConsoleReader : TextReader { public override string ReadLine() { if (!dialog.showInput(out string s, "", "Console.ReadLine", screen: screen.ofActiveWindow)) s = ""; return s; } public override int Read() { var s = _read; if (s.NE()) { if (!dialog.showInput(out s, "", "Console.Read", screen: screen.ofActiveWindow) || s == "") return -1; } char c = s[0]; _read = s[1..]; return (int)c; } string _read; } internal static void RedirectConsole_() { print.redirectConsoleOutput = true; Console.SetIn(new _ConsoleReader()); } } ================================================ FILE: Au/Internal/NamespaceDoc.cs ================================================ //These are not supported by DocFX, but used for code info. //rejected. Now code info for namespaces does not work. //namespace Au //{ // /// // /// Main classes of the automation library, except triggers. // /// // [CompilerGenerated] // class NamespaceDoc // { // } //} //namespace Au.Types //{ // /// // /// Types of function parameters, exceptions, etc used in the automation library. // /// // [CompilerGenerated] // class NamespaceDoc // { // } //} //namespace Au.Triggers //{ // /// // /// Triggers: hotkeys, autotext, mouse, window. // /// // [CompilerGenerated] // class NamespaceDoc // { // } //} //namespace Au.More //{ // /// // /// Rarely used classes of the automation library. // /// // [CompilerGenerated] // class NamespaceDoc // { // } //} ================================================ FILE: Au/Internal/NativeFont_.cs ================================================ namespace Au.More; /// /// Creates and manages native font handle. /// internal sealed class NativeFont_ : IDisposable { IntPtr _h; public IntPtr Handle => _h; public NativeFont_(IntPtr handle) { _h = handle; } public static implicit operator IntPtr(NativeFont_ f) => f?._h ?? default; ~NativeFont_() { _Dispose(); } public void Dispose() { _Dispose(); GC.SuppressFinalize(this); } void _Dispose() { if (_h != default) { Api.DeleteObject(_h); _h = default; } } public NativeFont_(int dpi, string name, int height, bool bold = false, bool italic = false) { _h = Api.CreateFont( -Math2.MulDiv(height, dpi, 72), cWeight: bold ? 700 : 0, //FW_BOLD bItalic: italic ? 1 : 0, //bUnderline: underline ? 1 : 0, //bStrikeOut: strikeout ? 1 : 0, iCharSet: 1, pszFaceName: name); } public unsafe NativeFont_(int dpi, bool bold, bool italic, int height = 0) { Api.LOGFONT m = default; Dpi.SystemParametersInfo(Api.SPI_GETICONTITLELOGFONT, sizeof(Api.LOGFONT), &m, dpi); if (bold) m.lfWeight = 700; if (italic) m.lfItalic = 1; if (height != 0) m.lfHeight = -Math2.MulDiv(height, dpi, 72); _h = Api.CreateFontIndirect(m); } public int HeightOnScreen { get { if (_heightOnScreen == 0) { using var dc = new FontDC_(_h); _heightOnScreen = dc.MeasureEP("A").height; } return _heightOnScreen; } } int _heightOnScreen; //flags: loword dpi, 0x10000 bold, 0x20000 italic static unsafe NativeFont_ _CreateCached(int flags) { int dpi = flags & 0xffff; bool bold = 0 != (flags & 0x10000), italic = 0 != (flags & 0x20000); //if (0 != (flags & 0x100000)) return new NativeFont_(dpi, "Verdana", 9, bold, italic); return new NativeFont_(dpi, bold, italic); } static readonly ConcurrentDictionary _d = new(); /// /// Cached standard font used by most windows and controls. /// Usually it is Segoe UI 9. /// internal static NativeFont_ RegularCached(int dpi) => _d.GetOrAdd(dpi, i => _CreateCached(i)); /// /// Cached standard bold font used by most windows and controls. /// internal static NativeFont_ BoldCached(int dpi) => _d.GetOrAdd(dpi | 0x10000, i => _CreateCached(i)); } //currently not used ///// ///// Provides various versions of standard UI font. ///// Per-monitor DPI-aware. ///// ///// ///// The properties return non-cached Font objects. It's safe to dispose them. It's OK to not dispose (GC will do; GDI+ fonts don't use much unmanaged memory). ///// //internal static class Fonts_ //{ // //info: we don't return cached Font objects, because we cannot protect them from disposing. The Font class is sealed. // // SystemFonts too, always creates new object. // // But eg Brushes and SystemBrushes use cached object. It is not protected from disposing (would be exception later). // /// // /// Standard font used by most windows and controls. // /// Usually it is Segoe UI 9. // /// // public static Font Regular(int dpi) => Font.FromHfont(NativeFont_.RegularCached(dpi)); // /// // /// Bold version of font. // /// // public static Font Bold(int dpi) => Font.FromHfont(NativeFont_.BoldCached(dpi)); //} ================================================ FILE: Au/Internal/NativeScrollbar_.cs ================================================ namespace Au.More; /// /// Manages native vertical or horizontal scrollbar of a window. /// internal /*abstract*/ class NativeScrollbar_ { readonly bool _vertical; readonly Func _itemStart, _itemEnd; wnd _w; int _pos, _max, _nItems, _offset; public NativeScrollbar_(bool vertical, Func itemStart, Func itemEnd) { _vertical = vertical; _itemStart = itemStart; _itemEnd = itemEnd; } //public NativeScrollbar_(bool vertical) { // _vertical = vertical; //} public bool Visible { get => _visible; set => _Show(value, true); } bool _visible; void _Show(bool show, bool api) { if (_visible != show) { _visible = show; if (!show) { if (_pos != 0 && !_w.Is0) Api.SCROLLINFO.SetPos(_w, _vertical, 0, false); _pos = _max = _offset = 0; } if (api && !_w.Is0) Api.ShowScrollBar(_w, _vertical ? Api.SB_VERT : Api.SB_HORZ, show); } } /// /// If need, shows or hides vertical and/or horizontal scrollbar of same window. /// When need to show or hide both, this function is 2 times faster than calling separately. /// public static void ShowVH(NativeScrollbar_ vert, bool showV, NativeScrollbar_ horz, bool showH) { Debug.Assert(vert._w == horz._w && vert._vertical && !horz._vertical); if (showV == vert.Visible && showH == horz.Visible) return; bool dif = showV != showH; vert._Show(showV, dif); horz._Show(showH, dif); if (!dif) Api.ShowScrollBar(vert._w, Api.SB_BOTH, showV); //2 times faster and 2 times less messages, except wm_paint } //public void SetRange(int max, int page) { // _max = max; // _pos = Math.Min(_pos, max); // Api.SCROLLINFO.SetRange(_w, _vertical, max + page - 1, page, true); //} //protected abstract int ItemStart(int i); //protected abstract int ItemEnd(int i); public void SetRange(int nItems) { _max = 0; _nItems = nItems; if (nItems > 1) { var rc = _w.ClientRect; int to = _itemEnd(_nItems - 1) - (_vertical ? rc.Height : rc.Width); for (int i = _nItems; --i >= 0;) { //how many items in the last page? if (_itemStart(i) < to) { _max = i + 1; break; } } } _SetPos(_pos); Api.SCROLLINFO.SetRange(_w, _vertical, _nItems - 1, _nItems - _max, true); } bool _SetPos(int pos) { pos = Math.Clamp(pos, 0, _max); if (pos == _pos) return false; _pos = pos; _offset = _pos == 0 ? 0 : _itemStart(_pos); return true; } void _Scroll(int pos, int part) { if (!_SetPos(pos)) return; Api.SCROLLINFO.SetPos(_w, _vertical, _pos, true); PosChanged?.Invoke(this, part); } /// /// Gets current scroll position (index of top visible item). /// Setter sets scroll position, clamped 0-Max. Does not invalidate the control. /// public int Pos { get => _pos; set => _Scroll(value, -1); } public int Offset => _offset; /// /// Gets max scroll position. Returns 0 if no scrollbar. /// public int Max => _max; /// /// Gets or sets item count. /// Use setter to set item count when there is no scrollbar; asserts !_visible. /// public int NItems { get => _nItems; set { Debug.Assert(!_visible); _nItems = value; } } /// /// When scrollbar position changed. /// The int parameter is event source: if scrollbar, it is one of Api.SB_ constants; if , it is -1; if wheel, it is -2 if down, -3 if up. /// public event Action PosChanged; public bool WndProc(wnd w, int msg, nint wParam, nint lParam) { _w = w; switch (msg) { case Api.WM_NCDESTROY: _w = default; break; case Api.WM_VSCROLL when _vertical: case Api.WM_HSCROLL when !_vertical: _WmScroll(Math2.LoWord(wParam)); return true; case Api.WM_MOUSEWHEEL: _WmScroll(Math2.HiShort(wParam) < 0 ? -2 : -3); return true; } return false; } void _WmScroll(int part) { if (!_visible) return; int pos = _pos; switch (part) { case Api.SB_THUMBTRACK: pos = Api.SCROLLINFO.GetTrackPos(_w, _vertical); break; case Api.SB_LINEDOWN: pos++; break; case Api.SB_LINEUP: pos--; break; case Api.SB_PAGEDOWN or Api.SB_PAGEUP: var rc = _w.ClientRect; int clientSize = _vertical ? rc.Height : rc.Width; if (part == Api.SB_PAGEDOWN) { for (int to = _offset + clientSize; pos < _nItems && _itemEnd(pos) <= to;) pos++; } else { for (int to = _offset - clientSize; pos >= 0 && _itemStart(pos) >= to;) pos--; pos++; } break; case Api.SB_TOP: pos = 0; break; case Api.SB_BOTTOM: pos = _max; break; case -2 or -3: int k = Api.SystemParametersInfo(Api.SPI_GETWHEELSCROLLLINES, 3); pos += part == -2 ? k : -k; break; default: return; } _Scroll(pos, part); } /// /// Calculates new focused item index when pressed key Down, Up, PageDown, PageUp, End or Home. /// /// Current focused item index. Can be -1. /// /// /// This scrollbar must be vertical. Asserts. /// Returns unchanged i (even if -1) if k isn't a navigation key or if cannot change focused item. /// Works like standard list controls. /// public int KeyNavigate(int i, KKey k) { Debug.Assert(_vertical); if (!_vertical || _nItems == 0) return i; switch (k) { case KKey.Home: i = 0; break; case KKey.End: i = int.MaxValue; break; case KKey.Down: i++; break; case KKey.Up: if (i < 0) i = int.MaxValue; else i--; break; case KKey.PageDown when i < _nItems - 1: case KKey.PageUp when i > 0: int clientHeight = _w.ClientRect.Height; if (k == KKey.PageDown) { int to = _offset + clientHeight; if (_itemEnd(i + 1) > to) to = _itemStart(i + 1) + clientHeight; while (i + 1 < _nItems && _itemEnd(i + 1) <= to) i++; } else { int to = _offset; if (i == _pos) to -= clientHeight; while (i - 1 >= 0 && _itemStart(i - 1) >= to) i--; } break; default: return i; } return Math.Clamp(i, 0, _nItems - 1); } } ================================================ FILE: Au/Internal/NativeThread_.cs ================================================ namespace Au.More; /// /// Represents a thread created using API CreateThread. /// Use when need thread handle/id ASAP, or to call APC easier. /// unsafe class NativeThread_ { nint _handle; int _tid; bool _sta, _threadInited; Action _proc; nint _initEvent; //much faster than with ManualResetEvent or ManualResetEventSlim ~NativeThread_() { Api.CloseHandle(_handle); if (_initEvent != 0) Api.CloseHandle(_initEvent); } /// /// Starts new background thread using API CreateThread. /// Note: in thread proc use , not a static field inited like s_thread = new NativeThread_(...);, because s_thread may be still null. /// /// Thread procedure. /// Set ApartmentState.STA. /// Wait now until the thread procedure calls . public NativeThread_(Action proc, bool sta = true, bool waitInited = false) { _proc = proc; _sta = sta; if (waitInited) _initEvent = Api.CreateEvent2(default, true, false, null); _handle = Api.CreateThread(default, 0, &_Thread, GCHandle.Alloc(this), 0, out _tid); if (waitInited) { Api.WaitForSingleObject(_initEvent, -1); Api.CloseHandle(_initEvent); _initEvent = 0; } } /// /// Gets thread handle. /// The finalizer closes the handle. The object is not garbage-collected while the thread procedure is running. /// public nint Handle => _handle; /// /// Gets thread id. /// public nint Id => _tid; public static NativeThread_ OfThisThread => t_ofThisThread; [ThreadStatic] static NativeThread_ t_ofThisThread; [UnmanagedCallersOnly] static uint _Thread(GCHandle param) { var t = param.Target as NativeThread_; param.Free(); t_ofThisThread = t; if (t._sta) Thread.CurrentThread.SetApartmentState(ApartmentState.STA); t._proc(); return 0; } /// /// The thread procedure must call this when finished thread initialization and going to run an alertable message loop. /// If constructor was called with waitInited true, it will return (stop waiting). /// If actions were queued, executes them now. /// Example: NativeThread_.OfThisThread.ThreadInited(); /// Note: don't use code like s_thread.ThreadInited();, because s_thread (inited like s_thread = new NativeThread_(...);) may be still null. /// public void ThreadInited() { if (_initEvent != 0) Api.SetEvent(_initEvent); _threadInited = true; if (t_queue is { } q) { t_queue = null; q.Invoke(); } } public void QueueAPC(Action a) { Api.QueueUserAPC(&_Apc, _handle, GCHandle.Alloc(a)); } [ThreadStatic] static Action t_queue; [UnmanagedCallersOnly] static void _Apc(GCHandle param) { var a = param.Target as Action; param.Free(); if (t_ofThisThread is { } t && t._threadInited) a(); else t_queue += a; } public static void AlertableMessageLoop() { for (; ; ) { var k = Api.MsgWaitForMultipleObjectsEx(0, null, -1, Api.QS_ALLINPUT, Api.MWMO_ALERTABLE | Api.MWMO_INPUTAVAILABLE); if (k == 0) { if (!wait.doEvents()) break; } else if (k != Api.WAIT_IO_COMPLETION) { Debug_.Print(k); break; } } } } ================================================ FILE: Au/Internal/PostToThisThread_.cs ================================================ namespace Au.More; /// /// Executes actions in same UI thread as ctor. /// Use . Cannot create more instances. /// class PostToThisThread_ { readonly wnd _w; readonly Queue _q = new(); PostToThisThread_() { _w = WndUtil.CreateWindowDWP_(messageOnly: true, t_wp = _WndProc); ManagedThreadId = Environment.CurrentManagedThreadId; } public static PostToThisThread_ OfThisThread => t_default ??= new(); [ThreadStatic] static PostToThisThread_ t_default; [ThreadStatic] static WNDPROC t_wp; public int ManagedThreadId { get; } public void Post(Action a) { bool post; lock (this) { post = _q.Count == 0; _q.Enqueue(a); } if (post) _w.Post(Api.WM_USER); } nint _WndProc(wnd w, int message, nint wParam, nint lParam) { switch (message) { case Api.WM_USER: //print.it(_q.Count); object o; lock (this) { o = _q.Count switch { 0 => null, 1 => _q.Dequeue(), _ => _q.ToArray() }; _q.Clear(); } switch (o) { case Action a: a(); break; case Action[] a: foreach (var f in a) f(); break; } return default; } return Api.DefWindowProc(w, message, wParam, lParam); } } ================================================ FILE: Au/Internal/ProcessStarter_.cs ================================================ using Microsoft.Win32.SafeHandles; namespace Au.More; unsafe struct ProcessStarter_ { public char[] cl; public Api.STARTUPINFO si; public uint flags; public string curDir; public string envVar; string _exe; //for errors only /// /// Prepares parameters for API CreateProcess and similar. /// /// /// Full path of program file. If not full path, uses . Uses . /// If rawExe true, does not use Normalize/ThisApp. /// /// null or command line arguments. /// /// Initial current directory of the new process. /// - If null, uses Directory.GetCurrentDirectory(). /// - Else if rawCurDir==true, uses raw curDir value. /// - Else if "", calls pathname.getDirectory(exe). /// - Else calls . /// /// null or environment variables to pass to the new process together with variables of this process. Format: "var1=value1\0var2=value2\0". If ends with "\0\0", will pass only these variables. /// Don't normalize exe. /// Don't normalize curDir. public ProcessStarter_(string exe, string args = null, string curDir = null, string envVar = null, bool rawExe = false, bool rawCurDir = false) { if (!rawExe) exe = pathname.normalize(exe, folders.ThisApp, PNFlags.DontExpandDosPath | PNFlags.DontPrefixLongPath); _exe = exe; cl = (args == null ? ("\"" + exe + "\"" + "\0") : ("\"" + exe + "\" " + args + "\0")).ToCharArray(); if (curDir == null) this.curDir = Directory.GetCurrentDirectory(); //if null passed to CreateProcessWithTokenW, the new process does not inherit current directory of this process else this.curDir = rawCurDir ? curDir : (curDir.Length == 0 ? pathname.getDirectory(exe) : pathname.expand(curDir)); si.cb = Api.SizeOf(); si.dwFlags = Api.STARTF_FORCEOFFFEEDBACK; flags = Api.CREATE_UNICODE_ENVIRONMENT; if (envVar != null && !envVar.Ends("\0\0")) { var es = Api.GetEnvironmentStrings(); int len1; for (var k = es; ; k++) if (k[0] == 0 && k[1] == 0) { len1 = (int)(k - es) + 2; break; } int len2 = envVar.Length; var t = new string('\0', len1 + len2); fixed (char* p = t) { MemoryUtil.Copy(es, p, --len1 * 2); for (int i = 0; i < envVar.Length; i++) p[len1 + i] = envVar[i]; } this.envVar = t; Api.FreeEnvironmentStrings(es); } else this.envVar = null; } /// /// Starts process using API CreateProcess or CreateProcessAsUser, without the feedback hourglass cursor. /// /// Receives CreateProcessX results. Will need to close handles in pi, eg pi.Dispose. /// If this process has UAC integrity level uiAccess, let the new process inherit it. /// API parameter bInheritHandles. public bool StartL(out Api.PROCESS_INFORMATION pi, bool inheritUiaccess = false, bool inheritHandles = false) { if (inheritUiaccess && Api.OpenProcessToken(Api.GetCurrentProcess(), Api.TOKEN_QUERY | Api.TOKEN_DUPLICATE | Api.TOKEN_ASSIGN_PRIMARY, out Handle_ hToken)) { using (hToken) return Api.CreateProcessAsUser(hToken, null, cl, null, null, inheritHandles, flags, envVar, curDir, si, out pi); } else { return Api.CreateProcess(null, cl, null, null, inheritHandles, flags, envVar, curDir, si, out pi); } } /// /// Starts process using API CreateProcess or CreateProcessAsUser, without the feedback hourglass cursor. /// /// Which field to set in Result. /// If this process has UAC integrity level uiAccess, let the new process inherit it. /// Failed. public Result Start(Result.Need need = 0, bool inheritUiaccess = false) { bool suspended = need == Result.Need.NetProcess && !_NetProcessObject.IsFast, resetSuspendedFlag = false; if (suspended && 0 == (flags & Api.CREATE_SUSPENDED)) { flags |= Api.CREATE_SUSPENDED; resetSuspendedFlag = true; } bool ok = StartL(out var pi, inheritUiaccess); if (resetSuspendedFlag) flags &= ~Api.CREATE_SUSPENDED; if (!ok) throw new AuException(0, $"*start process '{_exe}'"); return new Result(pi, need, suspended); } /// /// Starts UAC Medium integrity level (IL) process from this admin process. /// /// Which field to set in Result. /// Failed. /// /// Actually the process will have the same IL and user session as the shell process (normally explorer). /// Fails if there is no shell process (API GetShellWindow fails) for more than 2 s from calling this func. /// Asserts and fails if this is not admin/system process. Caller should at first call or . /// public Result StartUserIL(Result.Need need = 0) { if (s_userToken == null) { Debug.Assert(uacInfo.isAdmin); //else cannot set privilege if (!SecurityUtil.SetPrivilege("SeIncreaseQuotaPrivilege", true)) goto ge; //perf.first(); #if false //works, but slow, eg 60 ms, even if we don't create task everytime var s = $"\"{folders.ThisAppBS}{(osVersion.is32BitProcess ? "32" : "64")}\\AuCpp.dll\",Cpp_RunDll"; Au.WinTaskScheduler.Scheduler.CreateTaskToRunProgramOnDemand("Au", "rundll32", false, folders.System + "rundll32.exe", s); //Au.WinTaskScheduler.Scheduler.CreateTaskToRunProgramOnDemand("Au", "rundll32", false, folders.System + "notepad.exe"); //slow too //perf.next(); int pid = Au.WinTaskScheduler.Scheduler.RunTask("Au", "rundll32"); //perf.next(); //print.it(pid); var hUserProcess = Handle_.OpenProcess(pid); //print.it((IntPtr)hUserProcess); if(hUserProcess.Is0) goto ge; #else bool retry = false; g1: var w = Api.GetShellWindow(); if (w.Is0) { //if Explorer process killed or crashed, wait until it restarts if (!wait.until(2, () => !Api.GetShellWindow().Is0)) throw new AuException($"*start process '{_exe}' as user. There is no shell process."); 500.ms(); w = Api.GetShellWindow(); } var hUserProcess = Handle_.OpenProcess(w); if (hUserProcess.Is0) { if (retry) goto ge; retry = true; 500.ms(); goto g1; } //two other ways: //1. Enum processes and find one that has Medium IL. Unreliable, eg its token may be modified. //2. Start a service process. Let it start a Medium IL process like in QM2. Because LocalSystem can get token with WTSQueryUserToken. //tested: does not work with GetTokenInformation(TokenLinkedToken). Even if would work, in non-admin session it is wrong token. #endif //perf.nw(); using (hUserProcess) { if (Api.OpenProcessToken(hUserProcess, Api.TOKEN_DUPLICATE, out Handle_ hShellToken)) { using (hShellToken) { const uint access = Api.TOKEN_QUERY | Api.TOKEN_ASSIGN_PRIMARY | Api.TOKEN_DUPLICATE | Api.TOKEN_ADJUST_DEFAULT | Api.TOKEN_ADJUST_SESSIONID; if (Api.DuplicateTokenEx(hShellToken, access, null, Api.SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, Api.TOKEN_TYPE.TokenPrimary, out var userToken)) s_userToken = new SafeAccessTokenHandle(userToken); } } } if (s_userToken == null) goto ge; } bool suspended = need == Result.Need.NetProcess && !_NetProcessObject.IsFast, resetSuspendedFlag = false; if (suspended && 0 == (flags & Api.CREATE_SUSPENDED)) { flags |= Api.CREATE_SUSPENDED; resetSuspendedFlag = true; } bool ok = Api.CreateProcessWithTokenW(s_userToken.DangerousGetHandle(), 0, null, cl, flags, envVar, curDir, si, out var pi); if (resetSuspendedFlag) flags &= ~Api.CREATE_SUSPENDED; if (!ok) goto ge; return new Result(pi, need, suspended); ge: throw new AuException(0, $"*start process '{_exe}' as user"); } static SafeAccessTokenHandle s_userToken; /// /// Results of functions. /// public class Result { /// /// Which field to set. /// public enum Need { None, //NativeHandle, WaitHandle, NetProcess, } public int pid; public WaitHandle waitHandle; public Process netProcess; internal Result(in Api.PROCESS_INFORMATION pi, Need need, bool suspended) { pid = pi.dwProcessId; switch (need) { case Need.NetProcess: netProcess = _NetProcessObject.Create(pi, suspended: suspended); break; case Need.WaitHandle: pi.hThread.Dispose(); waitHandle = new WaitHandle_(pi.hProcess, true); break; default: pi.Dispose(); break; } } } /// /// Creates new .NET Process object with attached handle and/or id. /// static class _NetProcessObject //FUTURE: remove if unused { /// /// Returns true if can create such object in a fast/reliable way. Else will use Process.GetProcessById. /// It depends on .NET framework version, because uses private methods of Process class through reflection. /// public static bool IsFast { get; } = _CanSetHandleId(); public static bool _CanSetHandleId() { const BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod; s_mi1 = typeof(Process).GetMethod("SetProcessHandle", flags); s_mi2 = typeof(Process).GetMethod("SetProcessId", flags); if (s_mi1 != null && s_mi2 != null) return true; Debug.Assert(false); return false; } static MethodInfo s_mi1, s_mi2; /// /// Creates new .NET Process object with attached handle and/or id. /// Can be specified both handle and id, or one of them (then .NET will open process or get id from handle when need). /// public static Process Create(IntPtr handle, int id) { if (!IsFast) { if (id == 0) id = process.processIdFromHandle(handle); return Process.GetProcessById(id); //3 ms, much garbage, gets all processes, can throw } var p = new Process(); var o = new object[1]; if (handle != default) { o[0] = new SafeProcessHandle(handle, true); s_mi1.Invoke(p, o); } if (id != 0) { o[0] = id; s_mi2.Invoke(p, o); } return p; } /// /// Creates new .NET Process object with attached handle and id. /// Closes thread handle. If suspended, resumes thread. /// public static Process Create(in Api.PROCESS_INFORMATION pi, bool suspended) { try { return Create(pi.hProcess, pi.dwProcessId); } finally { if (suspended) Api.ResumeThread(pi.hThread); pi.hThread.Dispose(); } } } } ================================================ FILE: Au/Internal/Ptr_.cs ================================================ namespace Au.More; /// /// String functions for unmanaged char* or byte* strings. /// internal static unsafe class Ptr_ { #region char* /// /// Gets span from start to '\0'. /// public static RStr ToRSpan(char* p) => MemoryMarshal.CreateReadOnlySpanFromNullTerminated(p); /// /// Gets the number of characters in p until '\0'. /// /// '\0'-terminated string. Can be null. public static int Length(char* p) => MemoryMarshal.CreateReadOnlySpanFromNullTerminated(p).Length; /// /// Gets the number of characters in p until '\0' or max. /// /// '\0'-terminated string. Can be null if max is 0. /// Max length to scan. Returns max if does not find '\0'. public static int Length(char* p, int max) { int i = new RStr(p, max).IndexOf('\0'); return i < 0 ? max : i; } #endregion #region byte* /// /// Gets span from start to '\0'. /// public static RByte ToRSpan(byte* p) => MemoryMarshal.CreateReadOnlySpanFromNullTerminated(p); /// /// Gets the number of bytes in p until '\0'. /// /// '\0'-terminated string. Can be null. public static int Length(byte* p) => MemoryMarshal.CreateReadOnlySpanFromNullTerminated(p).Length; /// /// Gets the number of bytes in p until '\0' or max. /// /// '\0'-terminated string. Can be null if max is 0. /// Max length to scan. Returns max if does not find '\0'. public static int Length(byte* p, int max) { int i = new RByte(p, max).IndexOf((byte)0); return i < 0 ? max : i; } #endregion } ================================================ FILE: Au/Internal/Serializer_.cs ================================================ namespace Au.More; /// /// Binary-serializes and deserializes multiple values of types int, string, string[], byte[] and null. /// Used mostly for sending parameters for IPC through pipe etc. /// Similar to BinaryWriter, but faster and less garbage. Much faster than BinaryFormatter, CSV, etc. /// Serializes all values into a byte[] in single call. If need to append, use BinaryWriter instead. /// internal static unsafe class Serializer_ { /// /// Type of input and output values of functions. /// Has implicit conversions from/to int and string. /// public struct Value { public object Obj; public int Int; public VType Type; Value(int i) { Obj = null; Int = i; Type = VType.Int; } Value(object o, VType type) { Obj = o; Int = 0; Type = o != null ? type : VType.Null; } public static implicit operator Value(int i) => new Value(i); public static implicit operator Value(string s) => new Value(s, VType.String); public static implicit operator Value(string[] a) => new Value(a, VType.StringArray); public static implicit operator Value(byte[] a) => new Value(a, VType.ByteArray); public static implicit operator int(Value a) => a.Int; public static implicit operator string(Value a) => a.Obj as string; public static implicit operator string[](Value a) => a.Obj as string[]; public static implicit operator byte[](Value a) => a.Obj as byte[]; } public enum VType { Null, Int, String, StringArray, ByteArray } /// /// Serializes multiple values of types int, string, string[] and null. /// The returned array can be passed to . /// public static byte[] Serialize(params Value[] a) => _Serialize(false, a); /// /// Serializes multiple values of types int, string, string[] and null. /// Unlike , in the first 4 bytes writes the size of data that follows. /// Can be used with pipes or other streams where data size is initially unknown: read 4 bytes as int dataSize; var b=new byte[dataSize], read it, pass b to . /// public static byte[] SerializeWithSize(params Value[] a) => _Serialize(true, a); static byte[] _Serialize(bool withSize, Value[] a) { int size = 4; if (withSize) size += 4; for (int i = 0; i < a.Length; i++) { size++; switch (a[i].Type) { case VType.Int: size += 4; break; case VType.String: size += 4 + (a[i].Obj as string).Length * 2; break; case VType.StringArray: int z = 4; foreach (var v in a[i].Obj as string[]) z += 4 + v.Lenn() * 2; size += z; break; case VType.ByteArray: size += 4 + (a[i].Obj as byte[]).Length; break; } } var ab = new byte[size]; fixed (byte* b0 = ab) { byte* b = b0; if (withSize) { *(int*)b = ab.Length - 4; b += 4; } *(int*)b = a.Length; b += 4; for (int i = 0; i < a.Length; i++) { var ty = a[i].Type; *b++ = (byte)ty; switch (ty) { case VType.Int: *(int*)b = a[i].Int; b += 4; break; case VType.String: var s = a[i].Obj as string; _AddString(s); break; case VType.StringArray: var k = a[i].Obj as string[]; *(int*)b = k.Length; b += 4; foreach (var v in k) { if (v != null) _AddString(v); else { *(int*)b = -1; b += 4; } } break; case VType.ByteArray: var u = a[i].Obj as byte[]; *(int*)b = u.Length; b += 4; u.CopyTo(ab, b - b0); b += u.Length; break; } } Debug.Assert((b - b0) == size); void _AddString(string s) { *(int*)b = s.Length; b += 4; var c = (char*)b; for (int j = 0; j < s.Length; j++) c[j] = s[j]; b += s.Length * 2; } } return ab; } /// /// Deserializes values serialized by . /// Returns array of values passed to Serialize. /// public static Value[] Deserialize(RByte serialized) { fixed (byte* b0 = serialized) { byte* b = b0; int n = *(int*)b; b += 4; var a = new Value[n]; for (int i = 0; i < n; i++) { switch ((VType)(*b++)) { case VType.Null: break; case VType.Int: a[i] = *(int*)b; b += 4; break; case VType.String: a[i] = _GetString(); break; case VType.StringArray: var k = new string[*(int*)b]; b += 4; for (int j = 0; j < k.Length; j++) k[j] = _GetString(); a[i] = k; break; case VType.ByteArray: int len = *(int*)b; b += 4; a[i] = serialized.Slice((int)(b - b0), len).ToArray(); b += len; break; default: throw new ArgumentException(); } } return a; [MethodImpl(MethodImplOptions.AggressiveInlining)] string _GetString() { int len = *(int*)b; b += 4; if (len == -1) return null; var R = new string((char*)b, 0, len); b += len * 2; return R; } } } } ================================================ FILE: Au/Internal/SharedMemory_.cs ================================================ namespace Au.More; /// /// Memory shared by all processes using this library. /// [StructLayout(LayoutKind.Sequential, Size = c_size)] unsafe struct SharedMemory_ { #region variables used by our library classes //Declare variables used by our library classes. //Be careful: //1. Some type sizes are different in 32 and 64 bit process. // Solution: Use long and cast to IntPtr etc. For wnd use int. //2. The memory may be used by processes that use different library versions. // Solution: In new library versions don't change struct sizes and old members. // Maybe reserve some space for future members. If need more, add new struct. // Use eg [StructLayout(LayoutKind.Sequential, Size = 16)]. //reserve 16 for some header, eg shared memory version. [StructLayout(LayoutKind.Sequential, Size = 16)] struct _Header { } _Header _h; internal PrintServer.SharedMemoryData_ outp; internal Triggers.ActionTriggers.SharedMemoryData_ triggers; internal WindowsHook.SharedMemoryData_ winHook; internal perf.Instance perf; internal script.SharedMemoryData_ script; //internal ScriptEditor.SharedMemoryData_ editor; //public const int TasksDataSize_ = 0x4000; //internal struct TasksData_ { public int size; public fixed byte data[TasksDataSize_]; } //internal TasksData_ tasks; #endregion const int c_size = 0x200000; //2 MB static SharedMemory_() { Ptr = (SharedMemory_*)Mapping.CreateOrOpen("Au-memory-lib", c_size).Mem; } /// /// Pointer to the shared memory. /// public static readonly SharedMemory_* Ptr; /// /// Gets pointer to the shared memory "return data" buffer. /// Used by . /// public static byte* ReturnDataPtr => (byte*)Ptr + c_size / 2; /// /// Size of buffer, 1 MB. /// public const int ReturnDataSize = c_size / 2; /// /// Shared memory pointer and mapping handle. /// public struct Mapping : IDisposable { IntPtr _hMapping; void* _mem; public void* Mem => _mem; /// /// Created new memory. If false - opened existing. /// public bool Created { get; } internal Mapping(IntPtr h, void* m, bool created) { _hMapping = h; _mem = m; Created = created; } public void Dispose() { if (_mem != null) { Api.UnmapViewOfFile(_mem); _mem = null; } if (_hMapping != default) { Api.CloseHandle(_hMapping); _hMapping = default; } } /// /// Creates named shared memory of specified size. Opens if already exists. /// Returns Mapping variable that contains shared memory address in this process. /// /// Shared memory name. Case-insensitive. /// Shared memory size. Ignored if the shared memory already exists. /// The API failed. /// /// Calls API CreateFileMapping and API MapViewOfFile. /// The shared memory is alive at least until this process ends or the returned Mapping variable disposed. Other processes can keep the memory alive even after that. /// public static Mapping CreateOrOpen(string name, int size) { //CONSIDER: don't use Api.SECURITY_ATTRIBUTES.ForLowIL. Speed with it 2.7 ms, without 1.7 ms. // But then cannot use this library in low IL processes. Probably never used anyway. // Try to move everything to the cpp dll. var hm = Api.CreateFileMapping((IntPtr)~0, Api.SECURITY_ATTRIBUTES.ForLowIL, Api.PAGE_READWRITE, 0, (uint)size, name); if (!hm.Is0) { bool created = lastError.code != Api.ERROR_ALREADY_EXISTS; var mem = Api.MapViewOfFile(hm, 0x000F001F, 0, 0, 0); //FILE_MAP_ALL_ACCESS if (mem != default) return new(hm, mem, created); hm.Dispose(); } throw new AuException(0, "*open shared memory"); } public static bool TryOpenExisting(string name, out Mapping r) { r = default; var hm = Api.OpenFileMapping(0x2, false, name); //FILE_MAP_WRITE if (hm.Is0) return false; var mem = Api.MapViewOfFile(hm, 0x000F001F, 0, 0, 0); //FILE_MAP_ALL_ACCESS if (mem == default) { hm.Dispose(); return false; } r = new(hm, mem, false); return true; } } } ================================================ FILE: Au/Internal/StaTaskScheduler_.cs ================================================ // // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE.md file in the project root for full license information. // namespace Au.More; /// Provides a scheduler that uses STA threads. sealed class StaTaskScheduler_ : TaskScheduler, IDisposable { /// /// Static auto-created StaTaskScheduler_ instance with 4 threads. /// public static new StaTaskScheduler_ Default => _default.Value; readonly static Lazy _default = new Lazy(() => new StaTaskScheduler_(4)); //info: 3-4 is optimal for getting icons /// Stores the queued tasks to be executed by our pool of STA threads. private BlockingCollection _tasks; /// The STA threads used by the scheduler. private readonly List _threads; /// Initializes a new instance of the StaTaskScheduler class with the specified concurrency level. /// The number of threads that should be created and used by this scheduler. public StaTaskScheduler_(int numberOfThreads) { // Validate arguments if (numberOfThreads < 1) throw new ArgumentOutOfRangeException(nameof(numberOfThreads)); // Initialize the tasks collection _tasks = new BlockingCollection(); // Create the threads to be used by this scheduler var a = new List(numberOfThreads); for (int i = 0; i < numberOfThreads; i++) { var thread = new Thread(() => { // Continually get the next task and try to execute it. // This will continue until the scheduler is disposed and no more tasks remain. foreach (var t in _tasks.GetConsumingEnumerable()) { TryExecuteTask(t); } }) { IsBackground = true }; thread.SetApartmentState(ApartmentState.STA); a.Add(thread); } _threads = a; // Start all of the threads _threads.ForEach(t => t.Start()); } /// Queues a Task to be executed by this scheduler. /// The task to be executed. protected override void QueueTask(Task task) => // Push it into the blocking collection of tasks _tasks.Add(task); /// Provides a list of the scheduled tasks for the debugger to consume. /// An enumerable of all tasks currently scheduled. protected override IEnumerable GetScheduledTasks() => // Serialize the contents of the blocking collection of tasks for the debugger _tasks.ToArray(); /// Determines whether a Task may be inlined. /// The task to be executed. /// Whether the task was previously queued. /// true if the task was successfully inlined; otherwise, false. protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) => // Try to inline if the current thread is STA //Thread.CurrentThread.GetApartmentState() == ApartmentState.STA && // TryExecuteTask(task); false; //important. Never run in thread that calls Task.Wait or Task.Result. /// Gets the maximum concurrency level supported by this scheduler. public override int MaximumConcurrencyLevel => _threads.Count; /// /// Cleans up the scheduler by indicating that no more tasks will be queued. /// This method blocks until all threads successfully shutdown. /// public void Dispose() { if (_tasks != null) { // Indicate that no new tasks will be coming in _tasks.CompleteAdding(); // Wait for all threads to finish processing tasks foreach (var thread in _threads) thread.Join(); // Cleanup _tasks.Dispose(); _tasks = null; } } } ================================================ FILE: Au/Internal/StringBuilder_.cs ================================================ namespace Au.More; /// /// Provides a cached reusable instance of StringBuilder per thread. It's an optimization that reduces the number of instances constructed and collected. /// Used like using(new StringBuilder_(out var b)) { b.Append("example"); var s = b.ToString(); }. /// /// /// This is a modified copy of the .NET internal StringBuilderCache class. /// internal struct StringBuilder_ : IDisposable { StringBuilder _sb; /// /// The cached StringBuilder has this Capacity. The cache is not used if capacity is bigger. /// public const int Capacity = 2000; [ThreadStatic] private static StringBuilder t_cached; /// /// Gets a new or cached/cleared StringBuilder of the specified or bigger capacity. /// /// /// Min needed StringBuilder.Capacity. If less than Capacity (2000), uses Capacity. If more than Capacity, does not use the cache. /// Use this parameter only when the needed capacity is variable; else either use default if need default or smaller, or don't use StringBuilder_ if need bigger. /// public StringBuilder_(out StringBuilder sb, int capacity = Capacity) { if (capacity <= Capacity) { capacity = Capacity; var b = t_cached; if (b != null) { t_cached = null; b.Clear(); sb = _sb = b; return; } } sb = _sb = new StringBuilder(capacity); } /// /// Releases the StringBuilder to the cache. /// public void Dispose() { if (_sb.Capacity == Capacity) t_cached = _sb; _sb = null; } } ================================================ FILE: Au/Internal/Util_.cs ================================================ using System.Windows; namespace Au.More; static unsafe class Not_ { //internal static void NullCheck(this T t, string paramName = null) where T : class { // if (t is null) throw new ArgumentNullException(paramName); //} /// /// Same as ArgumentNullException.ThrowIfNull. /// It's pity, they removed operator !! from C# 11. /// internal static void Null(object o, [CallerArgumentExpression("o")] string paramName = null) { if (o is null) throw new ArgumentNullException(paramName); } internal static void Null(object o1, object o2, [CallerArgumentExpression("o1")] string paramName1 = null, [CallerArgumentExpression("o2")] string paramName2 = null) { if (o1 is null) throw new ArgumentNullException(paramName1); if (o2 is null) throw new ArgumentNullException(paramName2); } internal static void Null(object o1, object o2, object o3, [CallerArgumentExpression("o1")] string paramName1 = null, [CallerArgumentExpression("o2")] string paramName2 = null, [CallerArgumentExpression("o3")] string paramName3 = null) { if (o1 is null) throw new ArgumentNullException(paramName1); if (o2 is null) throw new ArgumentNullException(paramName2); if (o3 is null) throw new ArgumentNullException(paramName3); } internal static void Null(object o1, object o2, object o3, object o4, [CallerArgumentExpression("o1")] string paramName1 = null, [CallerArgumentExpression("o2")] string paramName2 = null, [CallerArgumentExpression("o3")] string paramName3 = null, [CallerArgumentExpression("o4")] string paramName4 = null) { if (o1 is null) throw new ArgumentNullException(paramName1); if (o2 is null) throw new ArgumentNullException(paramName2); if (o3 is null) throw new ArgumentNullException(paramName3); if (o4 is null) throw new ArgumentNullException(paramName4); } internal static void Null(void* o, [CallerArgumentExpression("o")] string paramName = null) { if (o is null) throw new ArgumentNullException(paramName); } internal static T NullRet(T o, [CallerArgumentExpression("o")] string paramName = null) where T : class { if (o is null) throw new ArgumentNullException(paramName); return o; } } static class WpfUtil_ { /// /// true if SystemParameters.HighContrast and ColorInt.GetPerceivedBrightness(SystemColors.ControlColor)<=0.5. /// public static bool IsHighContrastDark { get { if (!SystemParameters.HighContrast) return false; //fast, cached var col = (ColorInt)SystemColors.ControlColor; //fast, cached var v = ColorInt.GetPerceivedBrightness(col.argb, false); return v <= .5; } } } ================================================ FILE: Au/Internal/misc_.cs ================================================ namespace Au.More; /// /// Dictionary with case-insensitive string keys (uses ). /// /// /// This class can be used instead of . /// Unlike Dictionary, keys remain case-insensitive after deserialization (, etc). /// Another way to deserialize case-insensitive dictionaries - JsonObjectCreationHandling.Populate in serialization options or attribute. /// class DictionaryI_ : Dictionary { /// /// Calls base constructor with . /// public DictionaryI_() : base(StringComparer.OrdinalIgnoreCase) { } /// /// Calls base constructor with . /// public DictionaryI_(int capacity) : base(capacity, StringComparer.OrdinalIgnoreCase) { } } ================================================ FILE: Au/Internal/tables.cs ================================================ namespace Au.More { /// /// Lookup tables for various functions of this library. /// unsafe static class Tables_ { static Tables_() { var t = new byte[55]; for (int u = 0; u < t.Length; u++) { char c = (char)(u + '0'); if (c >= '0' && c <= '9') t[u] = (byte)u; else if (c >= 'A' && c <= 'F') t[u] = (byte)(c - ('A' - 10)); else if (c >= 'a' && c <= 'f') t[u] = (byte)(c - ('a' - 10)); else t[u] = 0xFF; } Hex = t; } /// /// Table for and co. /// public static readonly byte[] Hex; /// /// Native-memory char[0x10000] containing lower-case versions of the first 0x10000 characters. /// public static char* LowerCase { get { var v = _lcTable; if (v == null) _lcTable = v = Cpp.Cpp_LowercaseTable(); return v; } //why operator ??= cannot be used with pointers? } static char* _lcTable; //never mind: this library does not support ucase/lcase chars 0x10000-0x100000 (surrogate pairs). // Tested with IsUpper/IsLower: about 600 such chars exist. ToUpper/ToLower can convert 40 of them. Equals/StartsWith/IndexOf/etc fail. } } ================================================ FILE: Au/Other/PrintServer.cs ================================================ //#define NEED_CALLER //rejected. Too slow and generates much garbage. /* Single global server is supported in this user session. Single local server is supported in this process. Server does not implement an output window etc. It just collects messages and notifies an output window. Asynchronously. How global output server/client is implemented: Single server and multiple clients. Server receives messages sent by clients. Clients - processes that send text messages to the server. The server's process also can be client. For IPC is used mailslot, waitable timer and shared memory (SM). Server: Creates mailslot and timer. Sets a bool about it in SM. Waits for timer and reads messages from mailslot. For better reliability, also checks mailslot periodically. If messages available, notifies an output window. On exit clears the bool in SM. Client, when sending a text message: If the SM bool is not set - discards the message, and closes mailslot if was open. Else: If not still done, opens mailslot. Writes message to mailslot. Sets timer if not set. Uses another bool in SM to indicate that the timer is set; server clears it. How local output server/client is implemented: Similar to global. Differences: Single server and single client (the same process). Uses waitable timer, but not mailslot/SM. Instead of mailslot, adds messages directly to _messages. Instead of SM, uses static variables. */ namespace Au.More { /// /// Receives messages sent by . /// /// /// If server is global, clients can be multiple processes, including this. Else only this process. /// Works asynchronously, to make writing messages faster. /// When a client writes a message, the message arrives to the server with some delay and is placed in a queue. /// You then can get/remove messages from the queue (call ) and display them in a window (for example). /// You can be notified about new messages. /// /// Recommended setup (see example): /// 1. When your application starts, create a PrintServer instance and assign to a static variable. Call . /// 2. When your application creates its output window, call to set window/message for notifications. /// 3. In window procedure, when received the notification message, get/remove/display all new output messages. /// 4. Call when closing the window. /// /// /// Simple program with output window. /// { 1.s(); print.it("test after"); 1.s(); print.clear(); 1.s(); print.it("test after Clear"); }); /// /// Application.Run(new OutputFormExample()); /// _os.Stop(); /// } /// } /// ]]> /// public unsafe class PrintServer { //info: //Although global and local servers are implemented quite differently, the interface is almost the same. For this and other reasons I decided to use single class. //For local server, the thread and kernel timer would be not necessary. Instead could use just a user timer. But it has some limitations etc. readonly ConcurrentQueue _messages = new(); //all received and still not removed messages that were sent by clients when they call print.it etc Handle_ _mailslot; //used if global WaitableTimer _timer; //used always wnd _notifWnd; int _notifMsg; bool _isStarted; readonly bool _isGlobal; bool _isLocalTimer; /// /// If true, will receive output from all processes that don't have local server. /// public PrintServer(bool isGlobal) => _isGlobal = isGlobal; /// /// Starts server. /// /// false if server already exists (if global - in any process). /// Failed. public bool Start() { lock (this) { if (print.s_localServer != null) return false; if (_isGlobal) { var m = Api.CreateMailslot(MailslotName_, 0, 0, Api.SECURITY_ATTRIBUTES.ForLowIL); if (m.Is0) { var e = lastError.code; if (e == Api.ERROR_ALREADY_EXISTS) return false; //called not first time, or exists in another process throw new AuException(e, "*create mailslot"); } _mailslot = m; _CreateTimerAndThread(); SM_->isServer = 1; } else { _CreateTimerAndThread(); } print.s_localServer = this; //if global server, will work as local when writes same process _isStarted = true; } return true; } void _CreateTimerAndThread() { try { if (_isGlobal) _timer = WaitableTimer.Create(false, TimerName_); else _timer = WaitableTimer.Create(); run.thread(_Thread, sta: false).Name = "Au.PrintServer"; } catch { if (_isGlobal) _mailslot.Dispose(); _timer?.Close(); _timer = null; throw; } } /// /// Stops server. /// public void Stop() { lock (this) { if (!_isStarted) return; _isStarted = false; if (_isGlobal) { _mailslot.Dispose(); SM_->isServer = 0; } print.s_localServer = null; _timer?.Set(0); //break thread loop; use minimal time. //info: the thread will dispose _timer and set=null } } /// /// Calls . /// ~PrintServer() => Stop(); /// /// Sets window/message to be notified about server events. /// /// Your window that displays output, or any other window. Its window procedure on message should call until it returns false. See example in class help. /// Windows message to send to w when one or more output events are available. For example WM_USER or WM_APP. public void SetNotifications(wnd w, int message) { _notifMsg = message; _notifWnd = w; if (!w.Is0) _timer?.Set(30); } void _Thread() { try { for (int period = 1000; ;) { bool isTimerEvent = _timer.WaitOne(period); //true if timer event, false if timeout if (isTimerEvent) { if (_isGlobal) SM_->isTimer = 0; _isLocalTimer = false; } lock (this) { if (!_isStarted) { _timer.Dispose(); _timer = null; break; } if (_isGlobal) { //read messages from mailslot and add to _messages. Else messages are added directly to _messages. while (Api.GetMailslotInfo(_mailslot, null, out int nextSize, out var msgCount) && msgCount > 0) { //note: GetMailslotInfo makes Systeminformer show constant 24 B/s I/O total rate. Does not depend on period. _ReadMailslotMessage(nextSize); if (msgCount == 1) break; } } } if (!_notifWnd.Is0 && !_messages.IsEmpty) { //print.qm2.write($"{_messages.Count}, {_ToMB(_memSize)}, {_ToMB(GC.GetTotalMemory(false))}"); if (!_notifWnd.IsAlive) break; _notifWnd.Send(_notifMsg); } if (isTimerEvent) period = 50; //check after 50 ms, to avoid 1000 ms delay in case a client did not set timer because SM_->isTimer was still 1 although the timer was already signaled else period = 1000; //check every 1000 ms, for full reliability //Console.WriteLine($"{period}"); } } catch (Exception ex) { Debug_.Dialog(ex); } } [SkipLocalsInit] void _ReadMailslotMessage(int size) { using FastBuffer b = new(size + 4); //+4 for "\r\n" var p = b.p; bool ok = Api.ReadFile(_mailslot, p, size, out var readSize) && readSize == size; if (ok) { long time = 0; string s = null, caller = null; var mtype = (PrintServerMessageType)(*p++); switch (mtype) { case PrintServerMessageType.Write or PrintServerMessageType.TaskEvent: if (size < 10) { ok = false; break; } //type, time(8), lenCaller time = *(long*)p; p += 8; int lenCaller = *p++; if (lenCaller > 0) { if (10 + lenCaller * 2 > size) { ok = false; break; } caller = new string((char*)p, 0, lenCaller); p += lenCaller * 2; } int len = (size - (int)(p - b.p)) / 2; if (!NoNewline && mtype == PrintServerMessageType.Write) { char* r = (char*)(b.p + size); r[0] = '\r'; r[1] = '\n'; len += 2; } s = new string((char*)p, 0, len); break; case PrintServerMessageType.Clear or PrintServerMessageType.ScrollToTop when size == 1: break; default: ok = false; break; } Debug.Assert(ok); if (ok) _AddMessage(new PrintServerMessage(mtype, s, time, caller)); } } //static string _ToMB(long n) => Math.Round(n / 1048576d, 3).ToS(); /// /// Adds s directly to _messages and sets timer. /// If s is null, it is "Clear" command. /// Else if !NoNewline, appends "\r\n". /// Used with local server; also with global server when writes the server's process. /// internal void LocalWrite_(string s, long time = 0, string caller = null) { //Debug.Assert(!_isGlobal); if (!NoNewline && s != null) s += "\r\n"; var m = new PrintServerMessage(s == null ? PrintServerMessageType.Clear : PrintServerMessageType.Write, s, time, caller); _AddMessage(m, true); } /// /// Adds action directly to _messages and sets timer. /// Used with local server; also with global server when writes the server's process. /// internal void LocalAction_(PrintServerMessageType action) { //Debug.Assert(!_isGlobal); _AddMessage(new(action), true); } void _AddMessage(PrintServerMessage m, bool setTimer = false) { //_memSize += _GetMessageMemorySize(m); _messages.Enqueue(m); if (setTimer && !_isLocalTimer) { _timer?.Set(10); _isLocalTimer = true; } } //static int _GetMessageMemorySize(PrintServerMessage m) => 50 + m.Text.Lenn() * 2; //int _memSize; /// /// Gets next message and removes from the queue. /// /// false if there are no messages. /// /// Messages are added to an internal queue when clients call etc. They contain the text, time, etc. This function gets the oldest message and removes it from the queue. /// public bool GetMessage(out PrintServerMessage m) { if (!_messages.TryDequeue(out m)) return false; //_memSize -= _GetMessageMemorySize(m); return true; } /// /// Gets the count of messages in the queue. /// public int MessageCount => _messages.Count; /// /// Let messages don't end with "\r\n". /// /// /// This can be used for performance, to avoid string copying when using local server. Does not affect performance of global server. /// public bool NoNewline { get; set; } #if NEED_CALLER /// /// Let clients provide the caller method of "print" functions. /// Note: It makes these methods much slower, especially when thread stack is big. Also generates much garbage. To find caller method is used class. /// See also: . /// public bool NeedCallerMethod { get => _isGlobal ? (SM_->needCaller != 0) : _localNeedCaller; set { if(_isGlobal) SM_->needCaller = (byte)(value ? 1 : 0); else _localNeedCaller = value; } } bool _localNeedCaller; internal static bool NeedCallerMethod_ { get { var t = s_localServer; return (t != null) ? t.NeedCallerMethod : SM_->needCaller != 0; } } #endif /// /// Gets mailslot name like @"\\.\mailslot\Au.print\" + sessionId. /// internal static string MailslotName_ { get { if (_mailslotName == null) { _mailslotName = @"\\.\mailslot\Au.print\" + process.thisProcessSessionId.ToString(); } return _mailslotName; } } static string _mailslotName; /// /// Gets waitable timer name like "timer.Au.print". /// internal static string TimerName_ => "timer.Au.print"; /// /// Shared memory variables. Used with global server only. /// [StructLayout(LayoutKind.Sequential, Size = 16)] //note: this struct is in shared memory. Size must be same in all library versions. internal struct SharedMemoryData_ { public byte isServer, isTimer; #if NEED_CALLER public byte needCaller; #endif } internal static SharedMemoryData_* SM_ => &SharedMemory_.Ptr->outp; } } namespace Au { public static partial class print { [MethodImpl(MethodImplOptions.NoInlining)] //for stack trace static void _ServerWrite(string s) { Debug.Assert(s != null); Api.GetSystemTimeAsFileTime(out var time); string caller = script.name; #if NEED_CALLER if(PrintServer.NeedCallerMethod_) { //info: this func always called from directly, which is usually called through Writer, Write. But it is public and can be called directly. var k = new StackTrace(2); //skip this func and directly() lock(_writerTypes) { for(int i = 0, n = k.FrameCount; i < n; i++) { var m = k.GetFrame(i).GetMethod(); var t = m.DeclaringType; if(_writerTypes.Contains(t)) continue; caller = caller + ":" + t.Name + "." + m.Name; break; } } //speed: with 'new StackFrame(i)' usually slower, regardless of stack size. Faster only when 1 loop, maybe 2. //info: here we don't optimize caller strings like PrintServer does, because StackTrace creates much more garbage. } #endif var loc = s_localServer; if (loc != null) loc.LocalWrite_(s, time, caller); else s_client.WriteLine(s, PrintServerMessageType.Write, caller, time); } static void _ServerAction(PrintServerMessageType action) { var loc = s_localServer; if (loc == null) s_client.AddAction(action); else if (action == PrintServerMessageType.Clear) loc.LocalWrite_(null); else loc.LocalAction_(action); } static readonly _ClientOfGlobalServer s_client = new(); internal static PrintServer s_localServer; //null if we don't have a local server /// /// Logs start/end/fail events of miniProgram trigger actions. /// Editor displays it in the Recent tasks window, not in the output panel. /// Could also log other events. For example at first used for task start/end/fail events, but now it is implemented in editor. /// internal static void TaskEvent_(string s, long id, string sourceFile = null, int sourceLine = 0) { Debug.Assert(script.role == SRole.MiniProgram); //if (s == null) s = "\0DNl08ISh30Kbt6ekJV3VvA"; //JIT //now not used //else { if (sourceFile == null) sourceFile = script.s_idMainFile.ToString(); //task started/ended/failed else sourceFile = sourceFile + "\0" + sourceLine.ToS(); //trigger action started/ended/failed sourceFile = id.ToS() + "\0" + sourceFile; //} s_client.WriteLine(s, PrintServerMessageType.TaskEvent, sourceFile); } unsafe class _ClientOfGlobalServer { //info: the mailslot/timer are implicitly disposed when process ends. Handle_ _mailslot; WaitableTimer _timer; long _sizeWritten; [SkipLocalsInit] public void WriteLine(string s, PrintServerMessageType mtype, string caller = null, long time = 0) { if (time == 0) Api.GetSystemTimeAsFileTime(out time); lock (_lockObj1) { if (!_Connect()) return; int lenS = s.Length, lenCaller = (caller != null) ? Math.Min(caller.Length, 255) : 0; int lenAll = 1 + 8 + 1 + lenCaller * 2 + lenS * 2; //type, time, lenCaller, caller, s using FastBuffer b = new(lenAll); byte* p = b.p; //type *p++ = (byte)mtype; //time *(long*)p = time; p += 8; //caller *p++ = (byte)lenCaller; if (lenCaller != 0) { fixed (char* k = caller) MemoryUtil.Copy(k, p, lenCaller * 2); p += lenCaller * 2; } //s if (lenS != 0) fixed (char* k = s) MemoryUtil.Copy(k, p, lenS * 2); //s //if (s == "\0DNl08ISh30Kbt6ekJV3VvA") { //JIT //now not used // //_SetTimer(); // Jit_.Compile(typeof(WaitableTimer), nameof(WaitableTimer.Set)); // Jit_.Compile(typeof(Api), nameof(Api.WriteFile), nameof(Api.SetWaitableTimer)); //slow JIT SetWaitableTimer // return; //} g1: bool ok = Api.WriteFile(_mailslot, b.p, lenAll, out _); if (!ok && _ReopenMailslot()) goto g1; if (ok) { _SetTimer(); //prevent overflow of mailslot and _messages _sizeWritten += lenAll; if (_sizeWritten > 1_000_000) { while (Api.GetFileSizeEx(_mailslot, out _sizeWritten) && _sizeWritten > 300_000) Thread.Sleep(15); //note: these numbers are carefully adjusted for best performance etc } } } } public void AddAction(PrintServerMessageType action) { lock (_lockObj1) { if (!_Connect()) return; g1: byte b = (byte)action; bool ok = Api.WriteFile(_mailslot, &b, 1, out _); if (!ok && _ReopenMailslot()) goto g1; Debug.Assert(ok); if (ok) _SetTimer(); } } //If last error says that server's mailslot closed, closes client's mailsot/timer and tries to reopen. If reopened, returns true. bool _ReopenMailslot() { if (lastError.code == Api.ERROR_HANDLE_EOF) { //server's mailslot closed _Close(); if (_Connect()) return true; } else { Debug.Assert(false); } return false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] void _SetTimer() { if (PrintServer.SM_->isTimer == 0) { if (_timer.Set(10)) PrintServer.SM_->isTimer = 1; } } void _Close() { if (!_mailslot.Is0) { _mailslot.Dispose(); _timer.Close(); _timer = null; } } bool _Connect() { if (PrintServer.SM_->isServer == 0) { _Close(); return false; } if (_mailslot.Is0) { _mailslot = CreateFile_(PrintServer.MailslotName_, true); if (_mailslot.Is0) return false; _timer = WaitableTimer.Open(PrintServer.TimerName_, noException: true); if (_timer == null) { _mailslot.Dispose(); return false; } } return true; } } #if NEED_CALLER static readonly List _writerTypes = new List() { typeof(print), typeof(_OutputWriter) }; /// /// Introduces a class that contain methods designed to write to the output. /// Purpose - when server's is true, skip methods of this class when searching for the caller method in the call stack. /// For example, if you created class PrintColored that contains methods PrintRed, PrintGreen and PrintBlue, you should execute this code in its static constructor: print.introduceWriterClass(typeof(PrintColored));. /// Also use this if you redirect output using a writer class that calls directly(). /// Not used when writing to console or log file. /// public static void introduceWriterClass(Type t) { lock(_writerTypes) { if(!_writerTypes.Contains(t)) _writerTypes.Add(t); } } #endif } } namespace Au.Types { /// /// See . /// public enum PrintServerMessageType { /// /// Add line to the output window. /// All members can be used. /// Write, /// /// Clear the output window. /// Only is used. /// Clear, /// /// Used internally to log events such as start/end of trigger actions. /// TaskEvent, /// /// Scroll the output window to the top. /// Only is used. /// ScrollToTop, } /// /// Contains message text and/or related info. /// More info: , . /// public class PrintServerMessage { /// /// Message type (write, clear, etc). /// public PrintServerMessageType Type { get; } /// /// Message text. /// Used with . /// public string Text { get; set; } /// /// Message time in FILETIME format, UTC. /// Used with . /// To convert to string: DateTime.FromFileTimeUtc(m.TimeUtc).ToLocalTime().ToString(). /// public long TimeUtc { get; } #if NEED_CALLER /// /// The property value of the process that called . /// Used with . /// If is true, also includes the caller method. Format: "scriptname:type.method". /// public string Caller { get; } internal PrintServerMessage(PrintServerMessageType type, string text, long time, string caller) { Type = type; Text = text; TimeUtc = time; Caller = caller; } #else /// /// The property value of the process that called . /// Used with . /// public string Caller { get; } internal PrintServerMessage(PrintServerMessageType type, string text = null, long time = 0, string caller = null) { Type = type; Text = text; TimeUtc = time; Caller = caller; } #endif /// public override string ToString() { //in editor used for output history if (Type != PrintServerMessageType.Write) return ""; var k = DateTime.FromFileTimeUtc(TimeUtc).ToLocalTime(); return $"{k.ToString()} | {Caller}\r\n{Text}"; } } } ================================================ FILE: Au/Other/ScriptEditor.cs ================================================ namespace Au.More; /// /// Contains functions to interact with the script editor, if available. /// /// /// Functions of this class work when editor process is running, even if current process wasn't started from it. To detect whether current process was started from editor, use (it is null if not). /// public static class ScriptEditor { /// /// Finds editor's message-only window used with WM_COPYDATA etc. /// Uses or . /// internal static wnd WndMsg_ { get { var w = script.s_wndEditorMsg; if (!w.Is0) return w; return s_wndMsg.FindFast(null, c_msgWndClassName, true); } } static wnd.Cached_ s_wndMsg, s_wndMain; /// /// Class name of window. /// internal const string c_msgWndClassName = "Au.Editor.m3gVxcTJN02pDrHiQ00aSQ"; internal static wnd WndMain_(bool show = false) { var w = WndMsg_; return w.Is0 ? default : s_wndMain.Get(() => (wnd)w.Send(Api.WM_USER, 0, show ? 1 : 0)); } /// /// Returns true if editor is running. /// public static bool Available => !WndMsg_.Is0; /// /// The main editor window. /// /// Show the window (if the editor program is running). /// default(wnd) if the editor program isn't running or its main window still wasn't visible. public static wnd MainWindow(bool show = false) => WndMain_(show); /// /// Shows or hides the main editor window. /// /// ///
true - show, activate, restore if minimized. ///
false - hide. It does not hide the tray icon. ///
null - toggle. /// public static void ShowMainWindow(bool? show) { var w = WndMsg_; if (!w.Is0) w.Send(Api.WM_USER, 1, show switch { true => 1, false => 2, _ => 0 }); } /// /// Invokes an editor's menu command. /// /// Command name. If "" or invalid, prints all names. /// If it's a checkbox-item, true checks it, false unchecks, null toggles. Else must be null (default). /// Don't wait until the command finishes executing. For example, if it shows a modal dialog, don't wait until it is closed. /// Activate the main window. Default true. Some commands may not work correctly if the window isn't active. /// /// Shows the main window, regardless of activateWindow. Waits while it is disabled or not finished loading, unless script role is editorExtension and it runs in the editor's main thread (then not invoke the command). /// Does not invoke the command if the menu item is disabled or if it's a submenu-item. /// public static void InvokeCommand(string command, bool? check = null, bool dontWait = false, bool activateWindow = true) { var w = WndMsg_; if (w.Is0) return; int flags = check switch { true => 1, false => 2, _ => 0 } | _EditorExtensionFlag; if (activateWindow) flags |= 4; if (dontWait) flags |= 8; g1: nint r = WndCopyData.Send(w, 11, command, flags); if (r == -1) { _WaitWhileEditorDisabled(); goto g1; } } static int _EditorExtensionFlag => process.IsLaMainThread_ ? 16 : 0; static void _WaitWhileEditorDisabled() { MainWindow().WaitFor(0, o => o.IsEnabled()); } /// /// Gets the state of an editor's menu command (checked, disabled). /// /// Command name. If "" or invalid, prints all names. /// /// Shows the main window. Waits while it is disabled or not finished loading, unless script role is editorExtension and it runs in the editor's main thread (then returns Disabled). /// public static ECommandState GetCommandState(string command) { var w = WndMsg_; if (w.Is0) return 0; g1: nint r = WndCopyData.Send(w, 12, command, _EditorExtensionFlag); if (r == -1) { _WaitWhileEditorDisabled(); goto g1; } return (ECommandState)r; } /// /// Opens a script or other file. Also can move the text cursor. /// Does nothing if editor isn't running. /// /// A file in current workspace. Can be full path, or relative path in workspace, or file name with extension (".cs" etc). If folder, selects it. /// If not null, goes to this 1-based line index. /// If not null, goes to this 0-based column index in line (if line not null) or to this 0-based position in text (if line null). public static void Open([ParamString(PSFormat.FileInWorkspace)] string file, int? line = null, int? offset = null) { var w = WndMsg_; if (w.Is0) return; Api.AllowSetForegroundWindow(w.ProcessId); WndCopyData.Send(w, 4, $"{file}|{line}|{offset}"); } /// [Obsolete("use Open"), EditorBrowsable(EditorBrowsableState.Never)] public static void OpenAndGoToLine([ParamString(PSFormat.FileInWorkspace)] string file, int line) => Open(file, line); /// /// Gets icon string in specified format. /// /// Returns null if editor isn't running or if the file does not exist. Read more in Remarks. /// Script file/folder path etc, or icon name. See , . /// The format of input and output strings. /// /// If what is , this function tries to get icon XAML from assembly resources (passes file to , with color removed); if not found - from editor. By default the LibreAutomate compiler finds literal icon-like strings in code and adds icon XAML to assembly resources; see Properties > Resource > Options. /// public static string GetIcon(string file, EGetIcon what) => GetIcon_(file, what, false); internal static string GetIcon_(string file, EGetIcon what, bool skipResources) { if (IconNameToXaml_ is { } intx) return intx(file, what); if (what == EGetIcon.IconNameToXaml && script.role != SRole.EditorExtension && !skipResources) { if (IconString_.GetXamlFromResources(file) is string xaml) return xaml; } var w = WndMsg_; if (w.Is0) return null; WndCopyData.SendReceive(w, (int)Math2.MakeLparam(10, (int)what), file, out string r); return r; //rejected: add option to get serialized Bitmap instead. Now loads XAML in this process. It is 230 ms and +27 MB. // Nothing good if the toolbar etc also uses XAML icons directly, eg for non-script items. And serializing is slow. // Now not actual because of cache. } /// /// Editor sets this. Library uses it to avoid sendmessage when role editorExtension. /// internal static Func IconNameToXaml_; /// /// Returns true if the editor program is installed as [portable](xref:portable). /// /// /// Available in the script editor process and in scripts launched from it. Elsewhere false. /// /// If portable, these paths are different: /// - /// - /// - /// - /// - /// public static bool IsPortable { get; internal set; } /// /// Gets name, text and some info of the currently active file in editor. /// /// Need text too. /// null if there are no open files or if failed. public static EFileInfo GetFileInfo(bool needText) => GetFileInfo(null, needText); /// /// Gets name, text and some info of a file in the current workspace in editor. /// /// A file in current workspace. Can be full path, or relative path in workspace, or file name with extension (".cs" etc), or ":id". /// Need text too. /// null if the specified file not found or if failed. public static EFileInfo GetFileInfo(string file, bool needText) { var w = WndMsg_; var flags = needText ? "1" : "0"; if (!w.Is0 && WndCopyData.SendReceive(w, 14, file != null ? flags + " " + file : flags, out byte[] r)) { var x = Serializer_.Deserialize(r); string path = x[0]; return new EFileInfo(pathname.getName(path), path, x[1], (EFileKind)(int)x[2], (uint)(int)x[3], x[4], x[5]); } return null; } /// /// If a specified class file is currently active in editor, calls a callback function (which can be or call a function in that file) and returns true. /// /// /// List of tuples (string file, Action action): ///
file - a class file from current project; must be filename without ".cs" and path. ///
action - a callback function to call if that file is the active file in editor. /// /// /// A script project folder can contain one script file at the top, and any number of class files. When you click Run, the code execution starts from the script, even if a class file is currently active in editor. But sometimes you may want to execute just a function in the current class file, and skip script code. The example shows how to do it easily. /// /// /// Code at/near the start of the script file of a script project. /// TaskB.Func1(5)))) return; /// ]]> /// File TaskA.cs. /// /// File TaskB.cs. /// /// public static bool TestCurrentFileInProject(params (string file, Action action)[] files) { if (GetFileInfo(false) is { } f && f.kind is EFileKind.Class) { var s = f.name[..^3]; foreach (var (n, a) in files) { if (n.Eqi(s)) { a(); return true; } } } return false; } //rejected. Use folders.Editor. ///// ///// Gets some special folders of editor process. ///// ///// ///// Default folders are: ///// ThisAppDocuments - folders.Documents + "LibreAutomate". ///// ThisAppDataLocal - folders.LocalAppData + "LibreAutomate". ///// ThisAppTemp - folders.Temp + "LibreAutomate". ///// ///// Here path is either full path (like C:\folder or %folders.Documents%\folder) or path relative to the program's folder (like ChildFolder or ..\SiblingFolder). ///// ///// //public static class Folders { // static string _Get(int i) { // if (a == null) { // var w = WndMsg_; // if (!w.Is0) { // WndCopyData.SendReceive(w, 13, null, out string r); // if (!r.NE()) a = r.Split('|'); // } // a ??= new string[] { null, null, null }; // } // return a[i]; // } // static string[] a; // /// // /// Gets of editor process. // /// // /// null if failed. // public static FolderPath ThisAppDocuments => new(_Get(0)); // /// // /// Gets of editor process. // /// // /// null if failed. // public static FolderPath ThisAppDataLocal => new(_Get(1)); // /// // /// Gets of editor process. // /// // /// null if failed. // public static FolderPath ThisAppTemp => new(_Get(2)); //} //[StructLayout(LayoutKind.Sequential, Size = 64)] //note: this struct is in shared memory. Size must be same in all library versions. //internal struct SharedMemoryData_ { // int _wndEditorMsg, _wndEditorMain; // internal wnd wndEditorMsg { // get { // if (_wndEditorMsg != 0) { // var w = (wnd)_wndEditorMsg; // if (w.ClassNameIs(c_msgWndClassName)) return w; // //_wndEditorMsg = 0; //no, unsafe // } // return default; // } // set { _wndEditorMsg = (int)value; } // } // internal wnd wndEditorMain { // get => wndEditorMsg.Is0 ? default : (wnd)_wndEditorMain; // set { _wndEditorMain = (int)value; } // } //} } ================================================ FILE: Au/Other/internet.cs ================================================ using System.Net.NetworkInformation; using System.Net.Http; using System.Net.Http.Json; using System.Net.Http.Headers; using System.Text.Json; using System.Text.Json.Nodes; using System.Net; using System.IO.Compression; namespace Au { /// /// This class, together with (extension methods), make easier to use and other .NET Internet classes for tasks like download, post, ping. /// public static class internet { /// /// Sends an ICMP echo message to the specified website and returns true if successful. Can be used to check Internet connectivity. /// /// Domain name like "google.com" or IP like "123.45.67.89". /// Timeout in milliseconds. /// /// Not all websites support it. /// /// Uses . /// public static bool ping(string hostNameOrAddress = "google.com", int timeout = 5000) { try { using var ping = new Ping(); var reply = ping.Send(hostNameOrAddress, timeout); return reply.Status == IPStatus.Success; } catch { return false; } } //also tested http, but slow etc. /// /// Sends an ICMP echo message to the specified website and returns true if successful. Gets the roundtrip time. /// /// . /// public static bool ping(out int roundtripTime, string hostNameOrAddress = "google.com", int timeout = 5000) { roundtripTime = 0; try { using var ping = new Ping(); var reply = ping.Send(hostNameOrAddress, timeout); roundtripTime = (int)Math.Min(reply.RoundtripTime, int.MaxValue); return reply.Status == IPStatus.Success; } catch { return false; } } /// /// Gets a static instance that can be used in scripts to download web pages, post web form data, etc. /// /// /// Creates only the first time; later just returns it. /// /// Sets these properties and default headers: /// - = All. /// - User-Agent: Au. /// /// internet.http makes easier to discover and use internet get/post/etc functions when using this library. You can instead create an instance and use its functions in the same way. See the second example. Use the same HttpClient instance when making multiple get/post/etc requests. /// /// /// /// Without internet.http. /// /// Or. /// /// public static HttpClient http => _lazyHC.Value; //rejected: public setter static Lazy _lazyHC = new(_CreateHttpClient); static HttpClient _CreateHttpClient() { var h = new SocketsHttpHandler { AutomaticDecompression = DecompressionMethods.All }; var r = new HttpClient(h); r.DefaultRequestHeaders.Add("User-Agent", "Au"); //without it some servers reject requests process.thisProcessExit += _ => r.Dispose(); return r; //HttpClient does not close the connection after sending a request. It's good. Most servers have keep-alive timeout 5 or 10 s. // Closes when disposing. And maybe when server closes; and maybe after h.PooledConnectionIdleTimeout etc; not tested. // Does not close when sending request to another server. Supports multiple connections at the same time. //It seems HttpClient.Send etc are thread-safe. // Tested: if several threads send a request to the same URL, are created several connections. // However threads cannot safely change base address, default headers, etc. //Another possible problem - DNS changes. // By default, idle connections are closed after 1 minute. // For active connections can set h.PooledConnectionLifetime. Never mind. } /// /// Gets a static instance that can be used in this library. /// internal static HttpClient http_ => _lazyHC_.Value; static Lazy _lazyHC_ = new(_CreateHttpClient); /// /// Creates an for posting web form fields with functions like and . /// /// One or more web form field names and values. See example. /// An empty name. /// /// /// public static MultipartFormDataContent formContent(params (string name, object value)[] fields) { var m = new MultipartFormDataContent(); foreach (var (n, v) in fields) m.Add(n, v?.ToString()); return m; } //rejected: overload with params string[] fields. Not intuitive, need to learn how to separate names/values. Easy with tuples. //rejected. Does not support files and does not have significant advantages, just slightly smaller data to send. //public static FormUrlEncodedContent formContent2(params (string name, string value)[] fields) // => new(fields.Select(o=>new KeyValuePair(o.Item1, o.Item2))); /// /// Creates for posting JSON. It can be used with functions like and . /// /// JSON string, or (or a derived type), or object of any type that can be serialized to JSON with . /// /// Exceptions of . /// /// If x is string, creates/returns new . /// Else if x is (or a derived type), calls and creates/returns new . /// Else if x is (or a derived type), returns x. /// Else calls/returns . /// /// /// /// public static HttpContent jsonContent(object x, JsonSerializerOptions options = null) { return x switch { HttpContent c => c, string s => new StringContent(s, null, "application/json"), JsonNode n => new StringContent(n.ToJsonString(options), null, "application/json"), _ => JsonContent.Create(x, x.GetType(), null, options), }; } /// /// Creates for or in the same way as the extension methods of this library (ExtInternet.Get, ExtInternet.Post etc). /// /// System.Net.Http.HttpMethod.Post etc. /// /// /// /// public static HttpRequestMessage message(HttpMethod method, string url, IEnumerable headers = null, HttpContent content = null, string auth = null) => ExtInternet.HttpMessage_(method, url, headers, auth, null, content); /// /// Joins a URL address and parameters. Urlencodes parameters. /// /// URL part without parameters or with some parameters. The function does not modify it. /// URL parameters to append, like "name1=value1", "name2=value2". The function urlencodes them (). /// String like "address?name1=value1&name2=value2". /// Incorrect format of a parameters string. public static string urlAppend(string address, params string[] parameters) { var b = new StringBuilder(address); char sep = '?'; if (address != null && address.Contains('?')) sep = '&'; foreach (var s in parameters) { int i = s.IndexOf('='); if (i < 1) throw new ArgumentException("parameters must be like \"name1=value1\", \"name2=value2\""); b.Append(sep).Append(WebUtility.UrlEncode(s[..i])).Append('=').Append(WebUtility.UrlEncode(s[++i..])); sep = '&'; } return b.ToString(); } //rejected: overload with (string, string)[]. Longer code and no real advantages. //rejected: overload with Strings. Unsafe, limited, und unclear how to use correctly. //rejected: overload with FormattableString. The user code is less readable, may be even longer, unclear, easy to make bugs. Difficult to prevent encoding the address part. Dubious advantage - can use FormattableString directly for 'url' parameters of functions like 'Get'. } } namespace Au.Types { /// /// Extension methods for .NET Internet functions. /// public static class ExtInternet { /// /// Adds a non-file field. /// Uses . /// /// This. /// See . public static MultipartFormDataContent Add(this MultipartFormDataContent t, string name, string value) { t.Add(new StringContent(value), name); return t; } /// /// Adds a file field. /// Uses . /// Please read remarks about disposing. /// /// /// Field name. /// File path. /// Content-Type header, for example "image/png". /// Filename. If null, gets from file. /// This. /// /// Opens the file and stores the stream in this object. Won't auto-close it after uploading. To close files, dispose this MultipartFormDataContent object, for example with using like in the example. Else the file will remain opened/locked until this process exits or until next garbage collection. /// /// See . /// Exceptions of . /// /// /// public static MultipartFormDataContent AddFile(this MultipartFormDataContent t, string name, string file, string contentType = null, string fileName = null) { var k = new StreamContent(filesystem.loadStream(file)); if (contentType != null) k.Headers.ContentType = new MediaTypeHeaderValue(contentType); t.Add(k, name, fileName ?? pathname.getName(file)); return t; } /// /// Adds multiple HTTP request headers. /// Uses . /// /// /// Headers like "name1: value1", "name2: value2". /// Incorrect format of a headers string. /// Exceptions of . public static void AddMany(this HttpRequestHeaders t, params string[] headers) => AddMany(t, (IEnumerable)headers); /// public static void AddMany(this HttpRequestHeaders t, IEnumerable headers) { foreach (var s in headers) { int i = s?.IndexOf(':') ?? -1; if (i < 1) throw new ArgumentException("headers must be like \"name1: value1\", \"name2: value2\""); int j = i + 1; while (s.Eq(j, ' ')) j++; t.Add(s[..i], s[j..]); } } //rejected: overload with (string, string)[]. Longer code and no real advantages. //rejected: overload Strings. Shorter code if can safely uses string, but longer etc if need to use array because values may contain '|'. #region HttpClient /// /// Sends a GET request to the specified URL, and gets the response. /// /// /// URL. To create URL with urlencoded parameters you can use . /// Use . /// /// null or request headers like ["name1: value1", "name2: value2"]. /// Also you can add headers to , like internet.http.DefaultRequestHeaders.Add("User-Agent", "Script/1.0");. /// /// String like "username:password" for basic authentication. It will be encoded and sent in the Authorization header. For other authentication schemes use the headers parameter instead, like headers: ["Authorization: Bearer token"]. /// Can set more properties for the request. /// An HttpResponseMessage object that can be used to get response content (web page HTML, JSON, file, etc), headers etc. To get content use etc. /// /// Exceptions of . /// If headers used, exceptions of . /// /// Invalid URL format. /// /// /// public static HttpResponseMessage Get(this HttpClient t, string url, bool dontWait = false, IEnumerable headers = null, string auth = null, Action also = null) { using var m = HttpMessage_(HttpMethod.Get, url, headers, auth, also); return t.Send(m, dontWait ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead); } //rejected. It's easy to use .NET methods directly (for those familiar with await). For the same reason rejected CancellationToken parameter of Get etc. ///// //public static Task GetAsync(this HttpClient t, string url, bool dontWait = false, IEnumerable headers = null, string auth = null, Action also = null, CancellationToken cancel = default) { // using var m = _HttpMessage(HttpMethod.Get, url, headers, auth, also); // return t.SendAsync(m, dontWait ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead, cancel); //} internal static HttpRequestMessage HttpMessage_(HttpMethod method, string url, IEnumerable headers, string auth, Action also, HttpContent content = null) { Not_.Null(url); var m = new HttpRequestMessage(method, url); if (headers != null) m.Headers.AddMany(headers); if (auth != null) m.Headers.Add("Authorization", "Basic " + Convert.ToBase64String(auth.ToUTF8())); if (content != null) m.Content = content; also?.Invoke(m); return m; } //rejected. This could be used with 'also' parameter, and then could remove 'auth' parameter. But this library is mostly for non-programmers, and should make user code as simple as possible. ///// ///// Adds Authorization header for basic authentication. ///// //public static void Auth(this HttpRequestMessage t, string user, string password) { // t.Headers.Add("Authorization", "Basic " + Convert.ToBase64String($"{user}:{password}".ToUTF8())); //} /// /// Sends a GET request to the specified URL, and gets the response. Handles HTTP errors and exceptions. /// /// /// Receives HttpResponseMessage object that can be used to get response content (web page HTML, JSON, file, etc), headers etc. See example. Will be null if failed because of an exception. /// URL. To create URL with urlencoded parameters you can use . /// Use . /// If failed, call . /// /// false if failed. /// Invalid URL format. /// If headers used, exceptions of . /// /// /// public static bool TryGet(this HttpClient t, out HttpResponseMessage r, string url, bool dontWait = false, IEnumerable headers = null, bool printError = false, string auth = null, Action also = null) { using var m = HttpMessage_(HttpMethod.Get, url, headers, auth, also); try { r = t.Send(m, dontWait ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead); if (r.IsSuccessStatusCode) return true; if (printError) { var s1 = $"HTTP GET failed. {(int)r.StatusCode} ({r.StatusCode}), {r.ReasonPhrase}"; if (!dontWait) try { if (r.Text(ignoreError: true) is [_, ..] s2) s1 = s1 + ", " + s2; } catch { } print.warning(s1); } } catch (Exception e) { r = null; if (printError) print.warning($"HTTP GET failed. {e.ToStringWithoutStack()}"); } return false; } /// /// Sends a GET request to the specified URL, and gets the response. Saves the response content (file, web page, etc) in a file. /// /// /// URL. To create URL with urlencoded parameters you can use . /// File path. The function uses . Creates parent directory if need. /// /// An HttpResponseMessage object that contains response headers etc. Rarely used. /// /// Exceptions of and . /// If headers used, exceptions of . /// public static HttpResponseMessage Get(this HttpClient t, string url, string resultFile, IEnumerable headers = null, string auth = null, Action also = null) { Not_.Null(resultFile); var r = Get(t, url, true, headers, auth, also).Save(resultFile); r.Dispose(); return r; } /// /// Sends a POST request to the specified URL, and gets the response. /// /// /// URL. /// Data to post. Usually web form data (see ) or JSON (see ). Can be null. /// Use . /// /// An HttpResponseMessage object that can be used to get response content (web page HTML, JSON, file, etc), headers etc. To get content use etc. /// /// Exceptions of . /// If headers used, exceptions of . /// /// /// Post form data. /// Note: the using will close the file stream. Don't need it when content does not contain files. /// /// Post object as JSON. /// /// public static HttpResponseMessage Post(this HttpClient t, string url, HttpContent content, IEnumerable headers = null, bool dontWait = false, string auth = null, Action also = null) { using var m = HttpMessage_(HttpMethod.Post, url, headers, auth, also, content); return t.Send(m, dontWait ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead); } //rejected: bool disposeContent = true (auto-close MultipartFormDataContent streams etc). // Users may want to retry etc. Usually this is used in scripts that will exit soon. Usually not so important to close files immediately. //rejected. string resultFile = null. It seems POST is rarely used to download files. Also don't need parameter dontWait; HttpClient.PostAsync does not have it too. /// /// Sends a POST request to the specified URL, and gets the response. Handles HTTP errors and exceptions. /// /// /// Receives HttpResponseMessage object that can be used to get response content (web page HTML, JSON, file, etc), headers etc. See example. Will be null if failed because of an exception. /// URL. /// Data to post. Usually web form data (see ) or JSON (see ). Can be null. /// If failed, call . /// Use . /// /// false if failed. /// Invalid URL format. /// If headers used, exceptions of . /// /// Post form data. /// /// Post object as JSON. /// /// public static bool TryPost(this HttpClient t, out HttpResponseMessage r, string url, HttpContent content, IEnumerable headers = null, bool printError = false, bool dontWait = false, string auth = null, Action also = null) { using var m = HttpMessage_(HttpMethod.Post, url, headers, auth, also, content); try { r = t.Send(m, dontWait ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead); if (r.IsSuccessStatusCode) return true; if (printError) { var s1 = $"HTTP POST failed. {(int)r.StatusCode} ({r.StatusCode}), {r.ReasonPhrase}"; if (!dontWait) try { if (r.Text(ignoreError: true) is [_, ..] s2) s1 = s1 + ", " + s2; } catch { } print.warning(s1); } } catch (Exception e) { r = null; if (printError) print.warning($"HTTP POST failed. {e.ToStringWithoutStack()}"); } return false; } #endregion #region HttpResponseMessage /// /// Gets content text as string. Downloads it if need. /// /// /// Don't call . /// Failed HTTP request. No exception if ignoreError true. /// Exceptions of . public static string Text(this HttpResponseMessage t, bool ignoreError = false) { if (!ignoreError) t.EnsureSuccessStatusCode(); return t.Content.ReadAsStringAsync().Result_(); } /// /// Gets content data as byte[]. Downloads it if need. If content is text, the array contains that text, usually UTF-8. /// /// /// Don't call . /// Failed HTTP request. No exception if ignoreError true. /// Exceptions of . public static byte[] Bytes(this HttpResponseMessage t, bool ignoreError = false) { if (!ignoreError) t.EnsureSuccessStatusCode(); return t.Content.ReadAsByteArrayAsync().Result_(); } /// /// Parses content, which must be JSON, and returns the root node. Then you can access JSON elements like var y = (string)r["x"]["y"];. Downloads content if need. /// Uses . /// /// /// Don't call . /// Failed HTTP request. No exception if ignoreError true. /// Exceptions of . /// Failed to parse JSON. public static JsonNode Json(this HttpResponseMessage t, bool ignoreError = false) => JsonNode.Parse(Bytes(t, ignoreError)); /// /// Parses content, which must be JSON. From it creates/returns an object of type T. Downloads content if need. /// Uses . /// /// /// Failed HTTP request. /// Exceptions of ReadFromJsonAsync. public static T Json(this HttpResponseMessage t) => t.EnsureSuccessStatusCode().Content.ReadFromJsonAsync().Result_(); /// /// Saves content in a file. Downloads if need. /// /// /// File path. The function uses . Creates parent directory if need. /// This. /// Failed HTTP request. /// Not full path. /// Exceptions of , and other used functions. /// /// By default downloads content to a memory buffer before returning. To avoid it, use completionOption , or with dontWait true. Then call this function (it will download the file), and finally dispose the . /// /// public static HttpResponseMessage Save(this HttpResponseMessage t, string file) { t.EnsureSuccessStatusCode(); filesystem.createDirectoryFor(file = pathname.normalize(file)); using var s1 = t.Content.ReadAsStream(); using var s2 = File.Create(file); _GetDecompressStream(t, s1).CopyTo(s2); return t; //rejected: decompress in all funcs. Then cannot use ReadAsByteArrayAsync, ReadAsStringAsync, ReadFromJsonAsync. } static Stream _GetDecompressStream(HttpResponseMessage t, Stream s) { var ce = t.Content.Headers.ContentEncoding; if (ce.Count == 1) { switch (ce.First().Lower()) { case "br": return new BrotliStream(s, CompressionMode.Decompress); case "gzip": return new GZipStream(s, CompressionMode.Decompress); case "deflate": return new DeflateStream(s, CompressionMode.Decompress); } } return s; } /// /// Downloads content to stream and provides the progress. /// /// /// Writes to this stream. /// Calls this callback function to report the progress. If null, shows standard progress dialog. /// Can be used to cancel. /// Call stream.Dispose();. /// Progress dialog heading text. Default: "Downloading". /// false if canceled. /// Failed HTTP request. /// Other exceptions. /// /// By default downloads content to a memory buffer before returning. To avoid it, use completionOption , or with dontWait true. Then call this function (it will download the file), and finally dispose the . /// /// Cannot provide the progress percentage if the content length is unknown. Top reasons: /// - The HTTP server uses chunked transfer encoding. /// - The HTTP server uses content compression and the is configured to automatically decompress (for example ). Instead of internet.http create a and optionally set header "Accept-Encoding: br, gzip, deflate". This function will decompress. /// public static bool Download(this HttpResponseMessage t, Stream stream, Action progress = null, CancellationToken cancel = default, bool disposeStream = false, string progressText1 = null) { dialog pd = null; Stream decomp = null; try { ArgumentNullException.ThrowIfNull(stream, nameof(stream)); t.EnsureSuccessStatusCode(); if (cancel.IsCancellationRequested) return false; long size = t.Content.Headers.ContentLength ?? 0; //print.it(size); //print.it(t); //BAD: content length is unknown for many files if the HttpClient uses AutomaticDecompression (eg internet.http). // The header is removed, because impossible to know the decompressed length now. //BAD: content length is unknown if "Transfer-Encoding: chunked". Eg most webservers use chunked for html files. //GOOD: usually web servers don't use "Content-Encoding" and "Transfer-Encoding" for big files that are usually compressed, eg exe, zip, png. using var s1 = t.Content.ReadAsStream(); string filename = null; string _DialogText(long bytes) => $"{filename}\n{bytes / 1048576d:0.#} MB"; if (progress == null) { filename = pathname.getName(t.RequestMessage.RequestUri.AbsolutePath); pd = dialog.showProgress(size == 0, progressText1 ?? "Downloading", _DialogText(size)); } Stream stream1 = s1, stream2 = stream; string ce = t.Content.Headers.ContentEncoding.FirstOrDefault()?.Lower(); if (ce != null) { if (ce is not ("br" or "gzip" or "deflate") || t.Content.Headers.ContentEncoding.Count != 1) throw new NotSupportedException("Content-Encoding"); if (size > 0 && size < 10_000_000) stream2 = new MemoryStream((int)size); //to display progress we need a temp stream; finally will decompress it to *stream* else stream1 = decomp = _DecompStream(ce, s1); //can't display progress. Will decompress directly when reading. } static Stream _DecompStream(string ce, Stream s) => ce switch { "br" => new BrotliStream(s, CompressionMode.Decompress), "gzip" => new GZipStream(s, CompressionMode.Decompress), _ => new DeflateStream(s, CompressionMode.Decompress) }; var b = new byte[16384]; long have = 0; int n, ppercent = 0, ptime = Environment.TickCount; while ((n = stream1.Read(b)) > 0) { stream2.Write(b, 0, n); if (cancel.IsCancellationRequested) return false; have += n; int percent = 0, time = 0; bool updateProgress; if (size > 0) { percent = (int)(have * 100 / size); if (updateProgress = percent > ppercent) ppercent = percent; } else { time = Environment.TickCount; if (updateProgress = time - ptime > 200) ptime = time; } if (pd != null) { if (!pd.IsOpen) { pd = null; return false; } if (updateProgress) { if (size > 0) pd.Send.Progress(percent); else pd.Send.ChangeText2(_DialogText(have), resizeDialog: false); } } else { if (updateProgress) { ProgressArgs pa = new(size, have, percent); progress(pa); if (pa.Cancel) return false; } if (cancel.IsCancellationRequested) return false; } } if (size == 0) progress?.Invoke(new(have, have, 100)); if (stream2 != stream) { stream2.Position = 0; decomp = _DecompStream(ce, stream2); decomp.CopyTo(stream); } } finally { decomp?.Dispose(); if (disposeStream) stream.Dispose(); pd?.Send.Close(); } return true; } /// /// Downloads content to file and provides the progress. /// /// File path. The function uses . Creates parent directory if need. /// /// /// /// public static bool Download(this HttpResponseMessage t, string file, Action progress = null, CancellationToken cancel = default, string progressText1 = null) { t.EnsureSuccessStatusCode(); filesystem.createDirectoryFor(file = pathname.normalize(file)); return Download(t, File.Create(file), progress, cancel, disposeStream: true, progressText1); } /// /// The async version of . /// /// public static Task DownloadAsync(this HttpResponseMessage t, Stream stream, Action progress = null, CancellationToken cancel = default, bool disposeStream = false, string progressText1 = null) { if (stream == null || !t.IsSuccessStatusCode) { if (disposeStream) stream.Dispose(); ArgumentNullException.ThrowIfNull(stream, nameof(stream)); t.EnsureSuccessStatusCode(); } if (progress != null && SynchronizationContext.Current is { } ct && ct.GetType() != typeof(SynchronizationContext)) { //call in this thread bool canceled = false; var progress0 = progress; progress = pa => { //runs sync in task thread if (canceled) { pa.Cancel = true; return; } ct.Post(_ => { //runs async in caller's thread if (canceled) return; progress0(pa); if (pa.Cancel) canceled = true; }, null); }; //note: don't use Progress. With default synccontext it calls in random thread pool thread. // This code works like Progress with other synccontexts. } return Task.Run(() => Download(t, stream, progress, cancel, disposeStream, progressText1)); } /// /// The async version of . /// /// public static Task DownloadAsync(this HttpResponseMessage t, string file, Action progress = null, CancellationToken cancel = default) { t.EnsureSuccessStatusCode(); filesystem.createDirectoryFor(file = pathname.normalize(file)); var stream = File.Create(file); return DownloadAsync(t, stream, progress, cancel, disposeStream: true); } #endregion } /// /// Arguments for a progress callback function. /// /// The max expected value. Or 0 if unknown. /// The current value. /// Current * 100 / Total. Or 0 if unknown. public record class ProgressArgs(long Total, long Current, int Percent) { /// /// The callback function can use this to cancel the operation. /// public bool Cancel { get; set; } } } ================================================ FILE: Au/Other/lastError.cs ================================================ namespace Au { /// /// Gets, sets or clears the last error code of Windows API. Gets error text. /// /// /// Many Windows API functions, when failed, set an error code. Code 0 means no error. It is stored in an internal thread-specific int variable. But only if the API declaration's DllImport attribute has SetLastError = true. /// /// Some functions of this library simply call these API functions and don't throw exception when API fail. For example, most property-get functions. /// When failed, they return false/0/null/empty. Then you can use to get the error code or to get error text. /// /// Most of functions set error code only when failed, and don't clear the old error code when succeeded. Therefore may need to call before. /// /// Windows API error code definitions and documentation are not included in this library. You can look for them in API function documentation on the internet. /// /// /// /// public static class lastError { /// /// Calls API SetLastError(0), which clears the Windows API last error code of this thread. /// /// /// Need it before calling some functions if you want to use or . /// The same as lastError.code = 0;. /// public static void clear() => Api.SetLastError(0); /// /// Gets () or sets (API SetLastError) the Windows API last error code of this thread. /// public static int code { get => Marshal.GetLastWin32Error(); set => Api.SetLastError(value); } /// /// Gets the text message of the Windows API last error code of this thread. /// /// null if the code is 0. /// /// The string always ends with ".". /// public static string message => messageFor(code); /// /// Gets the text message of a Windows API error code. /// /// null if errorCode is 0. /// /// The string always ends with ".". /// public static unsafe string messageFor(int errorCode) { if (errorCode == 0) return null; if (errorCode == 1) return "The requested data or action is unavailable. (0x1)."; //or ERROR_INVALID_FUNCTION, but it's rare string s = "Unknown exception"; char* p = null; const uint fl = Api.FORMAT_MESSAGE_FROM_SYSTEM | Api.FORMAT_MESSAGE_ALLOCATE_BUFFER | Api.FORMAT_MESSAGE_IGNORE_INSERTS; int r = Api.FormatMessage(fl, default, errorCode, 0, &p, 0, default); if (p != null) { while (r > 0 && p[r - 1] <= ' ') r--; if (r > 0) { if (p[r - 1] == '.') r--; s = new string(p, 0, r); } Api.LocalFree(p); } s = (uint)errorCode <= 0xffff ? $"{s} ({errorCode})." : $"{s} (0x{errorCode:X})."; return s; } } } ================================================ FILE: Au/Other/opt.cs ================================================ namespace Au { /// /// Ambient options for some functions of this library. /// /// /// Some frequently used functions of this library have some options (settings). For example allows to change speed, text sending method, etc. Passing options as parameters in each call usually isn't what you want to do in automation scripts. Instead use this class. See examples. /// /// To store these options, internally is used . It means that a new task or thread inherits a copy of options of the caller. It can't modify options of the caller or other tasks/threads. /// /// /// /// Set options for trigger actions. /// { opt.key.KeySpeed = 50; }; /// ]]> /// public static class opt { /// /// Options for mouse functions (class and functions that use it). /// /// /// /// public static OMouse mouse => OMouse.Ambient_; /// /// Options for keyboard and clipboard functions (classes , and functions that use them). /// /// /// /// Use a instance. /// /// Set options for trigger actions. /// { opt.key.KeySpeed = 50; }; /// ]]> /// public static OKey key => OKey.Ambient_; /// /// Options for showing run-time warnings and other info that can be useful to find problems in code at run time. /// /// /// /// public static OWarnings warnings => OWarnings.Ambient_; /// /// Obsolete. Use instead. /// For backward compatibility, wait functions still use opt.wait.DoEvents if Seconds.DoEvents not specified. /// [EditorBrowsable(EditorBrowsableState.Never)] public static OWait wait => OWait.Ambient_; /// /// Obsolete. /// [Obsolete("Use opt instead. To set options for triggers can be used code like this: Triggers.Options.BeforeAction = o => { opt.key.TextSpeed = 5; opt.mouse.ClickSpeed = 30; };"), EditorBrowsable(EditorBrowsableState.Never)] public static class init { /// /// Obsolete. Same as . /// public static OMouse mouse => opt.mouse; /// /// Obsolete. Same as . /// public static OKey key => opt.key; /// /// Obsolete. Same as . /// public static OWarnings warnings => opt.warnings; } /// /// Creates temporary scopes for options. /// Example: using(opt.scope.key()) { opt.key.KeySpeed=5; ... }. /// public static class scope { /// /// Creates temporary scope for options. See example. /// /// If true (default), inherit current options. If false, uses default options. /// /// /// public static UsingEndAction mouse(bool inherit = true) { var old = OMouse.Scope_(inherit); return new UsingEndAction(() => OMouse.Ambient_ = old); } /// /// Creates temporary scope for options. See example. /// /// If true (default), inherit current options. If false, uses default options. /// /// /// public static UsingEndAction key(bool inherit = true) { var old = OKey.Scope_(inherit); return new UsingEndAction(() => OKey.Ambient_ = old); } /// /// Creates temporary scope for options. See example. /// /// If true (default), inherit current options. If false, uses default options. /// /// /// public static UsingEndAction warnings(bool inherit = true) { var old = OWarnings.Scope_(inherit); return new UsingEndAction(() => OWarnings.Ambient_ = old); } /// [EditorBrowsable(EditorBrowsableState.Never)] //obsolete public static UsingEndAction wait(bool inherit = true) { var old = OWait.Scope_(inherit); return new UsingEndAction(() => OWait.Ambient_ = old); } /// /// Creates temporary scope for all options. See example. /// /// If true (default), inherit current options. If false, uses default options. /// /// /// public static UsingEndAction all(bool inherit = true/*, int? speed = null*/) { var o1 = OMouse.Scope_(inherit); var o2 = OKey.Scope_(inherit); var o3 = OWarnings.Scope_(inherit); var o4 = OWait.Scope_(inherit); return new UsingEndAction(() => { OMouse.Ambient_ = o1; OKey.Ambient_ = o2; OWarnings.Ambient_ = o3; OWait.Ambient_ = o4; }); } } } } namespace Au.Types { /// /// Options for functions of class . /// /// /// Total Click(x, y) time is: mouse move + + button down + + button up + + . /// /// /// /// /// public sealed class OMouse { int _threadId; static AsyncLocal s_ambient = new(); internal static OMouse Ambient_ { get { var threadId = Environment.CurrentManagedThreadId; if (s_ambient.Value is { } v) { if (v._threadId != threadId) s_ambient.Value = v = new(v) { _threadId = threadId }; } else { s_ambient.Value = v = new() { _threadId = threadId }; } return v; } set { Debug.Assert(value?._threadId != 0); s_ambient.Value = value; } //used only to restore scope } internal static OMouse Scope_(bool inherit) { var old = s_ambient.Value; s_ambient.Value = (old != null && inherit) ? new(old) { _threadId = Environment.CurrentManagedThreadId } : null; //lazy return old; } struct _Fields { //makes easier to init, reset or copy fields public _Fields() { } public int ClickSpeed = 20, MoveSpeed, ClickSleepFinally = 10, MoveSleepFinally = 10; public bool Relaxed; } _Fields _f; /// /// Initializes this instance with default values or values copied from another instance. /// /// If not null, copies its options into this variable. internal OMouse(OMouse other = null) //don't need public like OKey { if (other != null) { _f = other._f; } else { _f = new(); } } int _Set(int value, int max) { if ((uint)value > max) throw new ArgumentOutOfRangeException(null, "Max " + max); return value; } /// /// How long to wait (milliseconds) after sending each mouse button down or up event (2 events for click, 4 for double-click). /// Default: 20. /// /// Valid values: 0 - 1000 (1 s). /// /// /// /// public int ClickSpeed { get => _f.ClickSpeed; set => _f.ClickSpeed = _Set(value, 1000); } /// /// If not 0, makes mouse movements slower, not instant. /// Default: 0. /// /// Valid values: 0 (instant) - 10000 (slowest). /// /// /// Used by , and other functions that generate mouse movement events, except . /// It is not milliseconds or some other unit. It adds intermediate mouse movements and small delays when moving the mouse cursor to the specified point. The speed also depends on the distance. /// Value 0 (default) does not add intermediate mouse movements. Adds at least 1 if some mouse buttons are pressed. Value 1 adds at least 1 intermediate mouse movement. Values 10-50 are good for visually slow movements. /// /// /// /// public int MoveSpeed { get => _f.MoveSpeed; set => _f.MoveSpeed = _Set(value, 10000); } /// /// How long to wait (milliseconds) before a "mouse click" or "mouse wheel" function returns. /// Default: 10. /// /// Valid values: 0 - 10000 (10 s). /// /// /// The "click" functions also sleep ms after button down and up. Default ClickSpeed is 20, default ClickSleepFinally is 10, therefore default click time without mouse-move is 20+20+10=50. /// /// /// /// public int ClickSleepFinally { get => _f.ClickSleepFinally; set => _f.ClickSleepFinally = _Set(value, 10000); } /// /// How long to wait (milliseconds) after moving the mouse cursor. Used in "move+click" functions too. /// Default: 10. /// /// Valid values: 0 - 1000 (1 s). /// /// /// Used by (finally), (between moving and clicking) and other functions that generate mouse movement events. /// /// /// /// public int MoveSleepFinally { get => _f.MoveSleepFinally; set => _f.MoveSleepFinally = _Set(value, 1000); } /// /// Make some functions less strict (throw less exceptions etc). /// Default: false. /// /// /// This option is used by these functions: /// - , and other functions that move the cursor (mouse pointer):\ /// false - throw exception if cannot move the cursor to the specified x y. For example if the x y is not in screen.\ /// true - try to move anyway. Don't throw exception, regardless of the final cursor position (which probably will be at a screen edge). /// - , and other functions that move the cursor (mouse pointer):\ /// false - before moving the cursor, wait while a mouse button is pressed by the user or another thread. It prevents an unintended drag-drop.\ /// true - do not wait. /// - and other functions that click or press a mouse button using window coordinates:\ /// false - don't allow to click in another window. If need, activate the specified window (or its top-level parent). If that does not help, throw exception. However if the window is a control, allow x y anywhere in its top-level parent window.\ /// true - allow to click in another window. Don't activate the window and don't throw exception. /// /// /// /// public bool Relaxed { get => _f.Relaxed; set => _f.Relaxed = value; } /// public override string ToString() => $"{{{string.Join(", ", GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Select(p => $"{p.Name} = {p.GetValue(this)}"))}}}"; } /// /// Options for functions of class . /// Some options also are used with functions that send keys (Ctrl+V etc). /// /// /// /// /// Set options for trigger actions. /// { opt.key.KeySpeed = 50; }; /// ]]> /// public sealed class OKey { int _threadId; static AsyncLocal s_ambient = new(); internal static OKey Ambient_ { get { var threadId = Environment.CurrentManagedThreadId; if (s_ambient.Value is { } v) { if (v._threadId != threadId) s_ambient.Value = v = new(v) { _threadId = threadId }; } else { s_ambient.Value = v = new() { _threadId = threadId }; } return v; } set { Debug.Assert(value?._threadId != 0); s_ambient.Value = value; } //used only to restore scope } internal static OKey Scope_(bool inherit) { var old = s_ambient.Value; s_ambient.Value = (old != null && inherit) ? new(old) { _threadId = Environment.CurrentManagedThreadId } : null; //lazy return old; } /// /// Initializes this instance with default values or values copied from another instance. /// /// If not null, copies its options into this variable. public OKey(OKey cloneOptions = null) { CopyOrDefault_(cloneOptions); } /// /// Copies options from o, or sets default if o==null. Like ctor does. /// internal void CopyOrDefault_(OKey o) { _f = o?._f ?? new(); } struct _Fields { //makes easier to init, reset or copy fields public _Fields() { } public int TextSpeed, KeySpeed = 2, KeySpeedClipboard = 5, SleepFinally = 10, PasteLength = 200, PasteSleep = 100/*, PasteRestoreAfter*/; public OKeyText TextHow = OKeyText.Characters; public bool TextShiftEnter, PasteWorkaround, RestoreClipboard = true, NoModOff, NoCapsOff, NoBlockInput; public Action Hook; } _Fields _f; /// /// Returns this variable, or OKey cloned from this variable and possibly modified by Hook. /// /// The focused or active window. Use GetWndFocusedOrActive. internal OKey GetHookOptionsOrThis_(wnd wFocus) { var call = this.Hook; if (call == null || wFocus.Is0) return this; var R = new OKey(this); call(new OKeyHookData(R, wFocus)); return R; } int _Set(int value, int max) { if ((uint)value > max) throw new ArgumentOutOfRangeException(null, "Max " + max); return value; } /// /// How long to wait (milliseconds) between pressing and releasing each character key. Used by . Also by and similar functions for "!text" arguments. /// Default: 0. /// /// Valid values: 0 - 1000 (1 second). /// /// /// Used only for "text" arguments, not for "keys" arguments. See . /// /// /// /// public int TextSpeed { get => _f.TextSpeed; set => _f.TextSpeed = _Set(value, 1000); } /// /// How long to wait (milliseconds) between pressing and releasing each key. Used by and similar functions, except for "!text" arguments. /// Default: 2. /// /// Valid values: 0 - 1000 (1 second). /// /// /// Used only for "keys" arguments, not for "text" arguments. See . /// /// /// /// public int KeySpeed { get => _f.KeySpeed; set => _f.KeySpeed = _Set(value, 1000); } /// /// How long to wait (milliseconds) between sending clipboard copy/paste keys. For example, when sending Ctrl+V, waits after Ctrl-down and after V-down. /// Default: 5. /// /// Valid values: 0 - 1000 (1 second). /// /// /// With most apps these delays are not necessary. But with some apps/controls Ctrl+V etc may not work without a delay after Ctrl-down or/and after V-down. /// /// /// /// public int KeySpeedClipboard { get => _f.KeySpeedClipboard; set => _f.KeySpeedClipboard = _Set(value, 1000); } /// /// How long to wait (milliseconds) before a "send keys or text" function returns. /// Default: 10. /// /// Valid values: 0 - 10000 (10 seconds). /// /// /// Not used by class functions. /// /// /// /// public int SleepFinally { get => _f.SleepFinally; set => _f.SleepFinally = _Set(value, 10000); } /// /// How to send text to the active window (keys, characters or clipboard). /// Default: . /// /// /// /// public OKeyText TextHow { get => _f.TextHow; set => _f.TextHow = value; } /// /// When sending text, instead of Enter send Shift+Enter. /// Default: false. /// /// /// This option is applied when sending text with (like keys.sendt("A\nB")) or with operator ! (like keys.send("!A\nB")) or with . Ignored when using operator ^, _, , . /// public bool TextShiftEnter { get => _f.TextShiftEnter; set => _f.TextShiftEnter = value; } /// /// To send text use clipboard (like with ) if text length is >= this value. /// Default: 200. /// /// /// /// /// public int PasteLength { get => _f.PasteLength; set => _f.PasteLength = _Set(value, int.MaxValue); } /// /// When pasting text that ends with space, tab or/and newline characters, remove them and after pasting send them as keys. /// Default: false. /// /// /// Some apps trim these characters when pasting. /// /// /// /// public bool PasteWorkaround { get => _f.PasteWorkaround; set => _f.PasteWorkaround = value; } //rejected: rarely used. Eg can be useful for Python programmers. Let call clipboard.paste() explicitly or set the Paste option eg in hook. ///// ///// To send text use if text contains characters '\n' followed by '\t' (tab) or spaces. ///// ///// ///// Some apps auto-indent. This option is a workaround. ///// //public bool PasteMultilineIndented { get; set; } /// /// Whether to restore clipboard data when copying or pasting text. /// Default: true. /// By default restores only text. See also , . /// /// /// /// /// /// /// public bool RestoreClipboard { get => _f.RestoreClipboard; set => _f.RestoreClipboard = value; } /// /// How long to wait (milliseconds) after pasting (before restoring clipboard data, if need). /// Default: 100. /// /// Valid values: 0 - 10000 (10 seconds). /// /// /// This is the minimal wait time. Actually may wait longer; it depends on how the active window responds. /// public int PasteSleep { get => _f.PasteSleep; set => _f.PasteSleep = _Set(value, 10000); } //CONSIDER ///// ///// After pasting ( etc), if true, restore clipboard data later (asynchronously) after this delay (milliseconds). ///// Default: 0. ///// ///// Valid values: 0 - 10000 (10 seconds). ///// //public int PasteRestoreAfter { // get => _f.PasteRestoreAfter; // set => _f.PasteRestoreAfter = _Set(value, 10000); //} #region static RestoreClipboard options /// /// When copying or pasting text, restore clipboard data of all formats that are possible to restore. /// Default: false - restore only text. /// /// /// Restoring data of all formats set by some apps can be slow or cause problems. More info: . /// /// This property is static, not thread-static. It should be set (if need) at the start of script and not changed later. /// /// /// /// /// /// public static bool RestoreClipboardAllFormats { get; set; } /// /// When copying or pasting text, and is true, do not restore clipboard data of these formats. /// Default: null. /// /// /// To restore clipboard data, the copy/paste functions at first get clipboard data. Getting data of some formats set by some apps can be slow (100 ms or more) or cause problems (the app can change something in its window or even show a dialog). /// It also depends on whether this is the first time the data is being retrieved. The app can render data on demand, when some app is retrieving it from the clipboard first time; then can be slow etc. /// /// You can use function to see format names and get-data times. /// /// There are several kinds of clipboard formats - registered, standard, private and display. Only registered formats have string names. For standard formats use API constant names, like "CF_WAVE". Private, display and metafile formats are never restored. /// These formats are never restored: CF_METAFILEPICT, CF_ENHMETAFILE, CF_PALETTE, CF_OWNERDISPLAY, CF_DSPx formats, CF_GDIOBJx formats, CF_PRIVATEx formats. Some other formats too, but they are automatically synthesized from other formats if need. Also does not restore if data size is 0 or > 10 MB. /// /// This property is static, not thread-static. It should be set (if need) at the start of script and not changed later. /// /// /// /// /// /// public static string[] RestoreClipboardExceptFormats { get; set; } /// /// Writes to the output some info about current clipboard data. /// /// /// Shows this info for each clipboard format: format name, time spent to get data (microseconds), data size (bytes), and whether this format would be restored (depends on ). /// Copy something to the clipboard each time before calling this function. Don't use and don't call this function in loop. Else it shows small times. /// The time depends on app, etc. More info: . /// /// /// /// public static void PrintClipboard() => clipboard.PrintClipboard_(); #endregion /// /// When starting to send keys or text, don't release modifier keys. /// Default: false. /// /// /// /// public bool NoModOff { get => _f.NoModOff; set => _f.NoModOff = value; } /// /// When starting to send keys or text, don't turn off CapsLock. /// Default: false. /// /// /// /// public bool NoCapsOff { get => _f.NoCapsOff; set => _f.NoCapsOff = value; } /// /// While sending or pasting keys or text, don't block user-pressed keys. /// Default: false. /// /// /// If false (default), user-pressed keys are sent afterwards. If true, user-pressed keys can be mixed with script-pressed keys, which is particularly dangerous when modifier keys are mixed (and combined) with non-modifier keys. /// /// /// /// public bool NoBlockInput { get => _f.NoBlockInput; set => _f.NoBlockInput = value; } /// /// Callback function that can modify options of "send keys or text" functions depending on active window etc. /// Default: null. /// /// /// The callback function is called by , , , and similar functions. Not called by . /// /// /// /// { /// print.it(k.w); /// var w = k.w.Window; //if k.w is a control, get its top-level window /// var name = w.Name; /// if (name.Like("* Slow App")) { /// k.optk.KeySpeed = 50; /// k.optk.TextSpeed = 50; /// } /// }; /// /// for (int i = 0; i < 10; i++) { /// 1.s(); /// keys.send("Home"); /// } /// ]]> /// public Action Hook { get => _f.Hook; set => _f.Hook = value; } /// public override string ToString() => $"{{{string.Join(", ", GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Select(p => $"{p.Name} = {p.GetValue(this)}"))}}}"; } /// /// Parameter type of the callback function. /// public struct OKeyHookData { internal OKeyHookData(OKey optk, wnd w) { this.optk = optk; this.w = w; } /// /// Options used by the "send keys or text" function. The callback function can modify them, except Hook, NoModOff, NoCapsOff, NoBlockInput. /// public readonly OKey optk; /// /// The focused control. If there is no focused control - the active window. Use w.Window to get top-level window; if w.Window == w, w is the active window, else the focused control. The callback function is not called if there is no active window. /// public readonly wnd w; } /// /// How functions send text. /// See . /// /// /// There are three ways to send text to the active app using keys: /// - Characters (default) - use special key code VK_PACKET. Can send most characters. /// - Keys - use virtual-key codes, with Shift etc where need. Can send only characters that can be simply entered with the keyboard using current keyboard layout. /// - Paste - use the clipboard and Ctrl+V. Can send any text. /// /// Most but not all apps support all three ways. /// public enum OKeyText { /// /// Send most text characters using special key code VK_PACKET. /// This option is default. Few apps don't support it. /// For newlines, tab and space sends keys (Enter, Tab, Space), because VK_PACKET often does not work well. /// If text contains Unicode characters with Unicode code above 0xffff, clipboard-pastes whole text, because many apps don't support Unicode surrogates sent as WM_PACKET pairs. /// Characters, //Tested many apps/controls/frameworks. Works almost everywhere. //Does not work with Pidgin (GTK), but works eg with Inkscape (GTK too). //I guess does not work with many games. //In PhraseExpress this is default. Its alternative methods are SendKeys (does not send Unicode chars) and clipboard. It uses clipboard if text is long, default 100. Allows to choose different for specified apps. Does not add any delays between chars; for some apps too fast, eg VirtualBox edit fields when text contains Unicode surrogates. /// /// Send virtual-key codes, with Shift etc where need. /// All apps support it. /// If a character cannot be simply typed with the keyboard using current keyboard layout, sends it like with the Characters option. /// KeysOrChar, /// /// Send virtual-key codes, with Shift etc where need. /// All apps support it. /// If text contains characters that cannot be simply typed with the keyboard using current keyboard layout, clipboard-pastes whole text. /// KeysOrPaste, /// /// Paste text using the clipboard and Ctrl+V. /// Few apps don't support it. /// This option is recommended for long text, because other ways then are too slow. /// Other options are unreliable when text length is more than 4000 and the target app is too slow to process sent characters. Then can help. /// Also, other options are unreliable when the target app modifies typed text, for example has such features as auto-complete, auto-indent or auto-correct. However some apps modify even pasted text, for example trim the last newline or space. /// When pasting text, previous clipboard data of some formats is lost. Text is restored by default. /// Paste, //rejected: WmPaste. Few windows support it. //rejected: WM_CHAR. It isn't sync with keyboard/mouse input. It has sense only if window specified (send to inactive window). Maybe will add a function in the future. } /// /// Options for run-time warnings (). /// /// /// /// public sealed class OWarnings { int _threadId; static AsyncLocal s_ambient = new(); internal static OWarnings Ambient_ { get { var threadId = Environment.CurrentManagedThreadId; if (s_ambient.Value is { } v) { if (v._threadId != threadId) s_ambient.Value = v = new(v) { _threadId = threadId }; } else { s_ambient.Value = v = new() { _threadId = threadId }; } return v; } set { Debug.Assert(value?._threadId != 0); s_ambient.Value = value; } //used only to restore scope } internal static OWarnings Scope_(bool inherit) { var old = s_ambient.Value; s_ambient.Value = (old != null && inherit) ? new(old) { _threadId = Environment.CurrentManagedThreadId } : null; //lazy return old; } bool? _verbose; List _disabledWarnings; /// /// Initializes this instance with default values or values copied from another instance. /// /// If not null, copies its options into this variable. internal OWarnings(OWarnings other = null) { if (other != null) { _verbose = other._verbose; _disabledWarnings = other._disabledWarnings == null ? null : new(other._disabledWarnings); } } /// /// If true, some library functions may display more warnings and other info. /// /// /// If not explicitly set, the default value depends on the build configuration of the main assembly: true if Debug, false if Release (optimize true). /// /// /// /// public bool Verbose { get => (_verbose ??= script.isDebug) == true; set => _verbose = value; } /// /// Disables one or more run-time warnings. /// /// One or more warnings as case-insensitive wildcard strings. See . /// /// Adds the strings to an internal list. When is called, it looks in the list. If finds the warning in the list, does not show the warning. /// It's easy to auto-restore warnings with using, like in the second example. Restoring is optional. /// /// /// /// Temporarily disable all warnings. /// /// public UsingEndAction Disable(params string[] warningsWild) { _disabledWarnings ??= new List(); int restoreCount = _disabledWarnings.Count; _disabledWarnings.AddRange(warningsWild); return new UsingEndAction(() => _disabledWarnings.RemoveRange(restoreCount, _disabledWarnings.Count - restoreCount)); } /// /// Returns true if the specified warning text matches a wildcard string added with . /// /// Warning text. Case-insensitive. public bool IsDisabled(string text) { string s = text ?? ""; if (_disabledWarnings is { } a) foreach (var k in a) if (s.Like(k, true)) return true; return false; } } /// /// Obsolete. Use instead. Some wait functions may still use some OWait properties for backward compatibility. /// [EditorBrowsable(EditorBrowsableState.Never)] public sealed class OWait { int _threadId; static AsyncLocal s_ambient = new(); internal static OWait Ambient_ { get { var threadId = Environment.CurrentManagedThreadId; if (s_ambient.Value is { } v) { if (v._threadId != threadId) s_ambient.Value = v = new(v) { _threadId = threadId }; } else { s_ambient.Value = v = new() { _threadId = threadId }; } return v; } set { Debug.Assert(value?._threadId != 0); s_ambient.Value = value; } //used only to restore scope } internal static OWait Scope_(bool inherit) { var old = s_ambient.Value; s_ambient.Value = (old != null && inherit) ? new(old) { _threadId = Environment.CurrentManagedThreadId } : null; //lazy return old; } internal OWait() { _period = 10; } internal OWait(OWait other) { _doEvents = other._doEvents; _period = other._period; } /// /// Obsolete. Use instead. /// public OWait(int? period = null, bool? doEvents = null) { _doEvents = doEvents ?? opt.wait._doEvents; _period = period ?? opt.wait._period; } /// /// Obsolete. Use instead. /// public bool DoEvents { get => _doEvents; set => _doEvents = value; } bool _doEvents; /// /// Obsolete. Used only by obsolete/hidden wait functions. Use instead. /// public int Period { get => _period; set => _period = value; } int _period; } } ================================================ FILE: Au/Other/print.cs ================================================ using COL = System.Collections; namespace Au; /// /// Writes text to the output window, console, log file or custom writer. /// [DebuggerStepThrough] public static partial class print { /// /// Returns true if this process is attached to a console. /// public static bool isConsoleProcess => Api.GetConsoleOutputCP() != 0; //fast //public static bool isConsoleProcess => Api.GetStdHandle(Api.STD_INPUT_HANDLE) is not (0 or -1); //no, may be true even if not attached to a console, eg if this non-console program started from cmd/bat on Win7 /// /// Returns true if is writing to console, false if to the output window etc. /// /// /// Does not write to console in these cases: /// - is false. /// - is true. /// - is not null. /// - The startup info of this process tells to not show console window and to not redirect the standard output. /// public static bool isWritingToConsole { get { if (!isConsoleProcess || ignoreConsole || logFile != null) return false; if (!_isVisibleConsole.HasValue) { Api.GetStartupInfo(out var x); _isVisibleConsole = x.hStdOutput != default || 0 == (x.dwFlags & 1) || 0 != x.wShowWindow; //redirected stdout, or visible console window } return _isVisibleConsole.Value; } } static bool? _isVisibleConsole; /// /// If true, in console process will not use the console window. Then everything is like in non-console process. /// /// /// public static bool ignoreConsole { get; set; } /// /// Clears the output window or console text (if ) or log file (if not null). /// public static void clear() { if (logFile != null) { _ClearToLogFile(); } else if (isWritingToConsole) { _ConsoleAction(PrintServerMessageType.Clear); } else if (qm2.use) { qm2.clear(); } else { _ServerAction(PrintServerMessageType.Clear); } } /// /// Scrolls the output window or console to the top. /// public static void scrollToTop() { if (logFile != null) { } else if (isWritingToConsole) { _ConsoleAction(PrintServerMessageType.ScrollToTop); } else if (qm2.use) { } else { _ServerAction(PrintServerMessageType.ScrollToTop); } } [MethodImpl(MethodImplOptions.NoInlining)] //avoid loading System.Console.dll static void _ConsoleAction(PrintServerMessageType action) { try { switch (action) { case PrintServerMessageType.Clear: //exception if redirected, it is documented. //if (!Console.IsOutputRedirected) Console.Clear(); //no, Clear does something more than if(IsOutputRedirected) Console.Clear(); break; case PrintServerMessageType.ScrollToTop: Console.SetCursorPosition(0, 0); break; } } catch { } } /// /// Writes string to the output. /// /// /// Appends newline ("\r\n"), unless text is like "<>text<nonl>". /// /// Can display links, colors, images, etc. More info: [](xref:output_tags). /// /// Where the text goes: /// - If redirected, to wherever it is redirected. See . /// - Else if using log file ( not null), writes to the file. /// - Else if using console ( returns true), writes to console. /// - Else if using local (in this process), writes to it. /// - Else if exists global (in any process), writes to it. /// - Else nowhere. /// public static void it(string value) { writer.WriteLine(value); } /// /// Writes value of any type to the output. /// /// Value of any type. If null, writes "null". /// /// If the type is unsigned integer (uint, ulong, ushort, byte, nuint), writes in hexadecimal format with prefix "0x", unless true. /// /// This overload is used for all types except: strings, arrays, generic collections. They have own overloads; to use this function need to cast to object. /// For and other ref struct types use print.it(x.ToString());. /// public static void it(object value) { it(util.toString(value)); } /// /// Writes interpolated string to the output. /// /// Interpolated string. Can contain :print format like in the example, to display the value like . /// /// /// public static void it(InterpolatedString value) { writer.WriteLine(value.GetFormattedText()); } /// /// Writes an array or generic collection to the output. /// /// /// Array or generic collection of any type. /// If null, writes "null". /// The format depends on type: ///
char[] - like string. ///
byte[] - like xx-xx-xx; in hexadecimal, unless true. ///
• Other - multiple lines. /// public static void it(IEnumerable value) { if (value is char[] or byte[]) print.it(util.toString(value)); else list("\r\n", value); } /// /// Writes an array or generic collection to the output, as a list of items separated by separator. /// /// Array or generic collection of any type. If null, writes "null". public static void list(string separator, IEnumerable value) { string s = "null"; if (value != null) using (new StringBuilder_(out var b)) { bool once = false; foreach (var v in value) { if (!once) once = true; else b.Append(separator); util.toString(b, v, compact: true); } s = b.ToString(); } print.it(s); } /// /// Writes multiple arguments of any type to the output, using separator ", ". /// /// /// If a value is null, writes "null". /// If a value is unsigned integer (uint, ulong, ushort, byte, nuint), writes in hexadecimal format with prefix "0x". /// public static void it(object value1, object value2, params object[] more) { it(util.toList(", ", value1, value2, more)); } /// /// Writes multiple arguments of any type to the output, using separator. /// /// public static void list(string separator, object value1, object value2, params object[] more) { it(util.toList(separator, value1, value2, more)); } /// /// Writes binary data to the output, formatted like in a hex editor. /// /// /// The number of bytes in a row. public static void it(RByte value, int columns) { it(util.toString(value, columns)); } /// /// Gets or sets object that actually writes text when is called . /// /// /// If you want to redirect or modify or just monitor output text, use code like in the example. It is known as "output redirection". /// Redirection is applied to whole process, not just this thread. /// Redirection affects , and . It does not affect and . /// Don't call in method WriteLine of your writer class. It would call itself and create stack overflow. Call , like in the example. /// /// /// Encoding.Unicode; /// } /// ]]> /// public static TextWriter writer { get; set; } = new _OutputWriter(); /// /// Our default writer class for the Writer property. /// class _OutputWriter : LineWriter_ { public override Encoding Encoding => Encoding.Unicode; protected override void WriteLineNow(string s) => directly(s); } /// /// Same as , but does not pass the string to . /// [MethodImpl(MethodImplOptions.NoInlining)] //for stack trace, used in _WriteToServer public static void directly(string value) { value ??= ""; //qm2.write($"'{value}'"); if (logFile != null) _WriteToLogFile(value); else if (isWritingToConsole) _ConsoleWriteLine(value); else if (qm2.use) qm2.write(value); else _ServerWrite(value); } [MethodImpl(MethodImplOptions.NoInlining)] //avoid loading System.Console.dll static void _ConsoleWriteLine(string value) => Console.WriteLine(value); /// /// Writes a warning text to the output. /// By default appends the stack trace. /// /// Warning text. /// If >= 0, appends the stack trace, skipping this number of frames. Default 0. Does not append if text looks like a stack trace. /// Text before text. Default "<>Warning: ". /// /// Calls . /// Does not show more than 1 warning/second, unless opt.warnings.Verbose == true (see ). /// To disable some warnings, use code opt.warnings.Disable("warning text wildcard"); (see ). /// /// [MethodImpl(MethodImplOptions.NoInlining)] public static void warning(string text, int showStackFromThisFrame = 0, string prefix = "<>Warning: ") { if (opt.warnings.IsDisabled(text)) return; if (!opt.warnings.Verbose) { var t = Api.GetTickCount64(); if (t - s_warningTime < 1000) return; s_warningTime = t; } string s = text ?? ""; if (showStackFromThisFrame >= 0 && !(s.Contains("\n at ") && s.RxIsMatch(@"Exception: .*\R at "))) { //include stack unless text contains stack var x = new StackTrace(showStackFromThisFrame + 1, true); var st = x.ToString(); var rn = st.Ends('\n') ? "" : "\r\n"; s = $"{prefix}{s} <\a>\r\n{st}{rn}"; } else s = prefix + s; it(s); } static long s_warningTime; /// /// Writes an exception warning to the output. /// /// [MethodImpl(MethodImplOptions.NoInlining)] public static void warning(Exception e, string prefix = "<>Warning: ") { warning(e.ToString(), -1, prefix); } /// /// Let Console.WriteX methods in non-console process write to the same destination as . /// /// /// The default value is true in non-console scripts that use class and have role miniProgram (default); also exeProgram if started from the script editor. Also in these scripts Console.ReadLine uses . /// /// If Console.Write text does not end with '\n' character, it is buffered and not displayed until called again with text ending with '\n' character or until called Console.WriteLine. /// /// Console.Clear will not clear output; it will throw exception. /// public static bool redirectConsoleOutput { set { if (value) { if (_prevConsoleOut != null || isConsoleProcess) return; _prevConsoleOut = Console.Out; Console.SetOut(writer); } else if (_prevConsoleOut != null) { Console.SetOut(_prevConsoleOut); _prevConsoleOut = null; } } get => _prevConsoleOut != null; } static TextWriter _prevConsoleOut; //note: don't call this before AllocConsole. Then can't restore, and IsOutputRedirected always returns true. /// /// Let , and similar methods also write to the same destination as . /// /// /// Does not replace existing Debug.Write etc destinations, just add new destination. /// /// If Debug.Write etc argument text does not end with '\n' character, it is buffered and not displayed until called again with text ending with '\n' character or until called Debug.WriteLine etc. /// /// Tip: To write to the output window even in console process, set print.ignoreConsole=true; before calling this method first time. /// public static bool redirectDebugOutput { set { if (value) { if (_traceListener != null) return; //Trace.Listeners.Add(IsWritingToConsole ? (new ConsoleTraceListener()) : (new TextWriterTraceListener(Writer))); Trace.Listeners.Add(_traceListener = new TextWriterTraceListener(writer)); //speed: 5000 } else if (_traceListener != null) { Trace.Listeners.Remove(_traceListener); _traceListener = null; } } get => _traceListener != null; } static TextWriterTraceListener _traceListener; /// /// Sets log file path. /// When set (not null), text passed to will be written to the file. /// If value is null - restores default behavior. /// /// /// The first etc call (in this process) creates or opens the file and deletes old content if the file already exists. /// /// Also supports mailslots. Use mailslot name, as documented in CreateMailslot. Multiple processes can use the same mailslot. /// /// The set function throws this exception if the value is not full path and not null. public static string logFile { get => _logFile; set { lock (_lockObj1) { if (_hFile != null) { _hFile.Close(); _hFile = null; } if (value != null) { _logFile = pathname.normalize(value); } else _logFile = null; } } } static string _logFile; static _LogFile _hFile; static readonly object _lockObj1 = new(); /// /// If true, will add current local time when using log file (see ). /// public static bool logFileTimestamp { get; set; } static void _WriteToLogFile(string s) { lock (_lockObj1) { if (_hFile == null) { g1: _hFile = _LogFile.Open(); if (_hFile == null) { var e = lastError.code; if (e == Api.ERROR_SHARING_VIOLATION) { var u = pathname.makeUnique(_logFile, false); if (u != _logFile) { _logFile = u; goto g1; } } var logf = _logFile; _logFile = null; print.warning($"Failed to create or open log file '{logf}'. {lastError.messageFor(e)}"); directly(s); return; } } _hFile.WriteLine(s); } } static void _ClearToLogFile() { lock (_lockObj1) { if (_hFile == null) { try { filesystem.delete(_logFile); } catch { } } else { _hFile.Clear(); } } } unsafe class _LogFile { //info: We don't use StreamWriter. It creates more problems than would make easier. // Eg its finalizer does not write to file. If we try to Close it in our finalizer, it throws 'already disposed'. // Also we don't need such buffering. Better to write to the OS file buffer immediately, it's quite fast. Handle_ _h; string _name; /// /// Opens logFile file handle for writing. /// Uses CREATE_ALWAYS, GENERIC_WRITE, FILE_SHARE_READ. /// public static _LogFile Open() { var path = logFile; var h = CreateFile_(path, false); if (h.Is0) return null; return new _LogFile() { _h = h, _name = path }; } /// /// Writes s + "\r\n" and optionally timestamp. /// /// /// If fails to write to file: Sets logFile=null, which closes file handle. Writes a warning and s to the output window or console. /// [SkipLocalsInit] public bool WriteLine(string s) { bool ok; int n = Encoding.UTF8.GetByteCount(s ??= "") + 1; using FastBuffer b = new(n + 35); byte* p = b.p; if (logFileTimestamp) { Api.GetLocalTime(out var t); Api.wsprintfA(p, "%i-%02i-%02i %02i:%02i:%02i.%03i ", __arglist(t.wYear, t.wMonth, t.wDay, t.wHour, t.wMinute, t.wSecond, t.wMilliseconds)); int nn = Ptr_.Length(p); Encoding.UTF8.GetBytes(s, new Span(p + nn, n)); n += nn; if (s.Starts("<>")) { Api.memmove(p + 2, p, nn); p[0] = (byte)'<'; p[1] = (byte)'>'; } } else { Encoding.UTF8.GetBytes(s, new Span(p, n)); } p[n - 1] = 13; p[n++] = 10; ok = Api.WriteFile(_h, p, n, out _); if (!ok) { string emsg = lastError.message; logFile = null; print.warning($"Failed to write to log file '{_name}'. {emsg}"); directly(s); //Debug.Assert(false); } return ok; } /// /// Sets file size = 0. /// public bool Clear() { bool ok = Api.SetFilePointerEx(_h, 0, null, Api.FILE_BEGIN) && Api.SetEndOfFile(_h); Debug.Assert(ok); return ok; } /// /// Closes file handle. /// public void Close() => _h.Dispose(); } /// /// Calls Api.CreateFile to open file or mailslot. /// /// File path or mailslot name. /// Use OPEN_EXISTING. If false, uses CREATE_ALWAYS. internal static Handle_ CreateFile_(string name, bool openExisting) { return Api.CreateFile(name, Api.GENERIC_WRITE, Api.FILE_SHARE_READ, openExisting ? Api.OPEN_EXISTING : Api.CREATE_ALWAYS); //tested: CREATE_ALWAYS works with mailslot too. Does not erase messages. Undocumented what to use. } /// #if !DEBUG [EditorBrowsable(EditorBrowsableState.Never)] #endif public static class qm2 { /// /// Sets to use QM2 as the output server. /// public static bool use { get; set; } /// /// Clears QM2 output panel. /// public static void clear() => _WriteToQM2(null); /// /// Writes line to QM2. /// public static void write(object o) => _WriteToQM2(o?.ToString() ?? ""); /// /// Writes multiple arguments of any type to the output, using separator ", ". /// /// /// If a value is null, writes "null". /// If a value is unsigned integer (uint, ulong, ushort, byte, nuint), writes in hexadecimal format with prefix "0x". /// public static void write(object value1, object value2, params object[] more) { write(util.toList(", ", value1, value2, more)); } /// /// The same as , but with [Conditional("DEBUG")]. /// [Conditional("DEBUG")] public static void writeD(object value1, object value2, params object[] more) { write(util.toList(", ", value1, value2, more)); } /// If null, clears output. static void _WriteToQM2(string s) { if (!_hwndQM2.IsAlive) { _hwndQM2 = Api.FindWindowEx(cn: "QM_Editor"); if (_hwndQM2.Is0) return; } _hwndQM2.Send(Api.WM_SETTEXT, -1, s); } static wnd _hwndQM2; } /// /// Write unsigned numeric types in decimal format, not hexadecimal. /// public static bool noHex { get; set; } /// /// Some functions used by the class. /// public static class util { /// /// Converts value of any type to string. Formats it like . /// /// Value of any type. If null, returns "null". /// If value is or , format it like "{ item1, item2 }". public static string toString(object value, bool compact = false) { switch (value) { case null: return "null"; case string t: return t; case ulong or uint or ushort or byte or nuint when !noHex: case COL.IEnumerable when !value.GetType().IsCOMObject: //info: eg Excel.Range and many other Excel interfaces are IEnumerable, and this process crashes, sometimes Excel too case COL.DictionaryEntry: using (new StringBuilder_(out var b)) { toString(b, value, compact); return b.ToString(); } default: return value.ToString(); } } /// /// Appends value of any type to StringBuilder. Formats it like . /// /// public static void toString(StringBuilder b, object value, bool compact) { switch (value) { case null: b.Append("null"); break; case string s: b.Append(s); break; case ulong u: _Unsigned(b, u); break; case uint u: _Unsigned(b, u); break; case ushort u: _Unsigned(b, u); break; case byte u: _Unsigned(b, u); break; case nuint u: _Unsigned(b, u); break; case char[] a: b.Append(a); break; case byte[] a: if (noHex) b.AppendJoin('-', a); else b.Append(BitConverter.ToString(a)); break; case COL.IEnumerable e when !value.GetType().IsCOMObject: if (compact) b.Append("{ "); string sep = null; foreach (var v in e) { if (sep == null) sep = compact ? ", " : "\r\n"; else b.Append(sep); toString(b, v, compact); } if (compact) b.Append(" }"); break; case COL.DictionaryEntry de: b.AppendFormat("[{0}, {1}]", de.Key, de.Value); break; default: b.Append(value); break; } static void _Unsigned(StringBuilder b, ulong u) { if (noHex) b.Append(u); else b.Append("0x").Append(u.ToString("X")); } } /// /// Converts multiple values of any type to string like . /// public static string toList(string sep, object value1, object value2, params object[] more) { if (more == null) more = s_oaNull; //workaround for: if the third argument is null, we receive null and not object[] { null } else if (more.GetType() != typeof(object[])) more = new object[] { more }; //workaround for: if the third argument is an array, prints its elements without { }. If empty array, prints nothing (even no comma). With this workaround - only if object[], which is rare; and it's good, because may be used in a wrapper function that passes its 'params object[]' parameter here. using (new StringBuilder_(out var b)) { for (int i = 0, n = 2 + more.Length; i < n; i++) { if (i > 0) b.Append(sep); util.toString(b, i == 0 ? value1 : (i == 1 ? value2 : more[i - 2]), compact: true); } return b.ToString(); //rejected: escape strings (eg if contains characters "\r\n,\0"): // it can damage formatting tags etc; // the string may be already escaped, eg wnd.ToString or elm.ToString; // we don't know whether the caller wants it; // let the caller escape it if wants, it's easy. } } static readonly object[] s_oaNull = { null }; /// /// Converts binary data to a hexadecimal + characters string, similar to the format used in hex editors. /// /// /// The number of bytes in a row. public static string toString(ReadOnlySpan data, int columns) { //rejected: , char escapeChar = '.' /// Character for bytes other than the printable ASCII characters (32-126). int len = data.Length; int rows = (len + columns - 1) / columns; var b = new StringBuilder(rows * (columns * 3 + 4 + columns + 2)); for (int i = 0; i < len; i += columns) { //hex for (int j = 0; j < columns && i + j < len; j++) { byte k = data[i + j]; b.Append(_ToHexChar(k >> 4)).Append(_ToHexChar(k & 0x0F)).Append(' '); } //padding if not enough columns for (int j = len - i; j < columns; j++) b.Append(" "); b.Append(" "); //text for (int j = 0; j < columns && i + j < len; j++) { byte k = data[i + j]; b.Append(k >= 32 && k <= 126 ? (char)k : '.'); } b.AppendLine(); } return b.ToString(); static char _ToHexChar(int h) => (char)(h < 10 ? h + '0' : h - 10 + 'A'); } } #pragma warning disable 1591 //no XML doc /// /// Interpolated string handler that adds :print format. See . /// [InterpolatedStringHandler, EditorBrowsable(EditorBrowsableState.Never)] public ref struct InterpolatedString { DefaultInterpolatedStringHandler _f; public InterpolatedString(int literalLength, int formattedCount) { _f = new(literalLength, formattedCount); } public InterpolatedString(int literalLength, int formattedCount, IFormatProvider provider) { _f = new(literalLength, formattedCount, provider); } public InterpolatedString(int literalLength, int formattedCount, IFormatProvider provider, Span initialBuffer) { _f = new(literalLength, formattedCount, provider, initialBuffer); } public void AppendLiteral(string value) => _f.AppendLiteral(value); public void AppendFormatted(T value) => _f.AppendFormatted(value); public void AppendFormatted(T value, int alignment) => _f.AppendFormatted(value, alignment); public void AppendFormatted(T value, string format) { if (format == "print") _f.AppendLiteral(util.toString(value, compact: true)); else _f.AppendFormatted(value, format); } public void AppendFormatted(T value, int alignment, string format) { if (format == "print") _f.AppendFormatted(util.toString(value, compact: true), alignment); else _f.AppendFormatted(value, alignment, format); } public void AppendFormatted(RStr value) => _f.AppendFormatted(value); public void AppendFormatted(RStr value, int alignment = 0, string format = null) => _f.AppendFormatted(value, alignment, format); public void AppendFormatted(string value) => _f.AppendFormatted(value); public void AppendFormatted(string value, int alignment = 0, string format = null) => _f.AppendFormatted(value, alignment, format); public void AppendFormatted(object value, int alignment = 0, string format = null) => _f.AppendFormatted(value, alignment, format); public string GetFormattedText() => _f.ToStringAndClear(); } } ================================================ FILE: Au/Other/screen.cs ================================================ namespace Au { /// /// Represents a screen device. Gets its rectangle etc. /// /// /// A computer can have one or more screens (aka display devices, monitors). One of them is the primary screen; its top-left coordinate is 0 0. /// To show or find a window or some object in a particular screen, need to identify the screen somehow. At Windows API level each screen has a unique integer identifier, known as screen handle or HMONITOR. But it is a random variable value and therefore cannot be specified directly in script etc. Instead can be used screen index or some object on that screen (window, point, rectangle). /// /// A screen variable can contain either a screen handle or a callback function that returns a screen handle. If empty, most functions interpret it as the primary screen. /// /// To create screen variables use static functions (like screen.index(1) or screen.primary) or constructors (like new screen(()=>screen.index(1))) or . Then call non-static functions to get screen properties. /// /// A screen handle cannot be reliably used for a long time. Screen handles may change when changing the configuration of multiple screens. Consider a "lazy" variable, ie with callback function . Then, whenever a function needs a screen handle, it calls the callback function which returns a screen with fresh handle. /// public struct screen : IEquatable { readonly IntPtr _h; readonly Func _func; /// /// Creates variable with screen handle, aka HMONITOR. /// public screen(IntPtr handle) { _h = handle; _func = null; } /// /// Creates "lazy" variable that calls your function to get screen when need. /// public screen(Func f) { _h = default; _func = f; } /// /// Gets the screen handle, aka HMONITOR. Returns default(IntPtr) if it wasn't set; see . /// public IntPtr Handle => _h; /// /// Gets the callback function that returns when need. Returns null if it wasn't set. /// public Func LazyFunc => _func; /// /// Returns true if this variable has no screen handle and no callback function. /// public bool IsEmpty => _h == default && _func == null; IntPtr _Handle() { if (_h != default) return _h; if (_func != null) return _func()._Handle(); return primary._h; } /// /// Returns a copy of this variable with . /// /// /// If this variable has , returns its clone. Else if has , calls it. Else gets the primary screen. /// public screen Now => new(_Handle()); /// /// Gets the primary screen. /// /// /// The returned variable has . To create lazy variable (with ), use screen.index(0, lazy: true). /// public static screen primary => new(Api.MonitorFromWindow(default, SODefault.Primary)); //fast /// /// Returns a lazy variable that later will get the screen from the mouse cursor position at that time. /// /// /// If need non-lazy: screen.of(mouse.xy) or screen.ofMouse.Now. /// public static screen ofMouse => new(s_ofMouse); /// /// Returns a lazy variable that later will get the screen of the active window at that time. /// /// /// If need non-lazy: screen.of(wnd.active) or screen.ofActiveWindow.Now. /// public static screen ofActiveWindow => new(s_ofActiveWindow); static readonly Func s_ofMouse = () => of(mouse.xy); static readonly Func s_ofActiveWindow = () => of(wnd.active); internal bool IsOfMouse_ => ReferenceEquals(_func, s_ofMouse); internal bool IsOfActiveWindow_ => ReferenceEquals(_func, s_ofActiveWindow); /// /// Gets screen containing the biggest part of the specified window or nearest to it. /// /// Window or control. If default(wnd) or invalid, gets the primary screen. /// /// /// Create variable with that later will get screen handle. /// Other ways to create lazy: ///
• use . Example: screen.of(new wndFinder("* Notepad")). ///
• use constructor. Example: new screen(() => screen.of(wnd.findFast(cn: "Notepad"))). /// public static screen of(wnd w, SODefault defaultScreen = SODefault.Nearest, bool lazy = false) => lazy ? new screen(() => of(w, defaultScreen)) : new screen(Api.MonitorFromWindow(w, defaultScreen)); /// /// Gets screen containing the biggest part of the specified window or nearest to it. /// /// Window finder. If window not found, gets the primary screen. /// /// Create variable with that later will find window and get screen handle. Default true. public static screen of(wndFinder f, SODefault defaultScreen = SODefault.Nearest, bool lazy = true) => lazy ? new screen(() => of(f, defaultScreen, false)) : of(f.Find(), defaultScreen); /// /// Gets screen containing the biggest part of the specified winforms window or control or nearest to it. /// /// Window or control. If handle not created, gets the primary screen. Cannot be null. /// /// Create variable with that later will get screen handle. public static screen of(System.Windows.Forms.Control c, SODefault defaultScreen = SODefault.Nearest, bool lazy = false) => lazy ? new screen(() => of(c, defaultScreen)) : of(c.Hwnd(), defaultScreen); /// /// Gets screen containing the biggest part of the specified WPF window or nearest to it. /// /// WPF window. If handle not created, gets the primary screen. Cannot be null. /// /// Create variable with that later will get screen handle. public static screen of(System.Windows.Window w, SODefault defaultScreen = SODefault.Nearest, bool lazy = false) => lazy ? new screen(() => of(w, defaultScreen)) : of(w.Hwnd(), defaultScreen); /// /// Gets screen containing the biggest part of the specified WPF element (of its rectangle) or nearest to it. /// /// WPF element. If not loaded, gets the primary screen. Cannot be null. /// /// Create variable with that later will get screen handle. public static screen of(System.Windows.FrameworkElement elem, SODefault defaultScreen = SODefault.Nearest, bool lazy = false) => lazy ? new screen(() => of(elem, defaultScreen)) : of(elem.RectInScreen(), defaultScreen); /// /// Gets screen containing the specified point or nearest to it. /// /// /// /// Create variable with that later will get screen handle. public static screen of(POINT p, SODefault defaultScreen = SODefault.Nearest, bool lazy = false) => lazy ? new screen(() => of(p, defaultScreen)) : new screen(Api.MonitorFromPoint(p, defaultScreen)); /// /// Gets screen containing the specified point or nearest to it. /// /// /// /// /// Create variable with that later will get screen handle. public static screen of(int x, int y, SODefault defaultScreen = SODefault.Nearest, bool lazy = false) => lazy ? new screen(() => of(x, y, defaultScreen)) : of((x, y), defaultScreen); /// /// Gets screen containing the biggest part of the specified rectangle or nearest to it. /// /// /// /// Create variable with that later will get screen handle. public static screen of(RECT r, SODefault defaultScreen = SODefault.Nearest, bool lazy = false) => lazy ? new screen(() => of(r, defaultScreen)) : new screen(Api.MonitorFromRect(r, defaultScreen)); /// /// Gets screens at various positions relative to the primary screen. /// public static class at { /// Gets a screen nearest to the top edge of the primary screen. /// Create variable with that later will get screen handle. public static screen top(bool lazy = false) => _S(0, -700, lazy); /// Gets a screen nearest to the bottom edge of the primary screen. /// Create variable with that later will get screen handle. public static screen bottom(bool lazy = false) => _S(0, 700, lazy); /// Gets a screen nearest to the left edge of the primary screen. /// Create variable with that later will get screen handle. public static screen left(bool lazy = false) => _S(-1000, 0, lazy); /// Gets a screen nearest to the right edge of the primary screen. /// Create variable with that later will get screen handle. public static screen right(bool lazy = false) => _S(1000, 0, lazy); /// Gets a screen nearest to the top-left corner of the primary screen. /// Create variable with that later will get screen handle. public static screen topLeft(bool lazy = false) => _S(-1000, -700, lazy); /// Gets a screen nearest to the top-right corner of the primary screen. /// Create variable with that later will get screen handle. public static screen topRight(bool lazy = false) => _S(1000, -700, lazy); /// Gets a screen nearest to the bottom-left corner of the primary screen. /// Create variable with that later will get screen handle. public static screen bottomLeft(bool lazy = false) => _S(-1000, 700, lazy); /// Gets a screen nearest to the bottom-right corner of the primary screen. /// Create variable with that later will get screen handle. public static screen bottomRight(bool lazy = false) => _S(1000, 700, lazy); } /// /// Gets screen at (or nearest to) the specified offset from the primary screen (PS). /// /// Horizontal offset. Negative is to the left from the left edge of PS. Positive is to the right from the right edge of PS. Zero is the horizontal center of PS. /// Vertical offset. Negative is up from the top edge of PS. Positive is down from the bottom edge of PS. Zero is the vertical center of PS. static screen _S(int dx, int dy, bool lazy) { var r = primary.Rect; if (dx > 0) dx += r.right; else if (dx == 0) dx = r.CenterX; if (dy > 0) dy += r.bottom; else if (dy == 0) dy = r.CenterY; return of(dx, dy, lazy: lazy); } /// /// Gets all screens. /// /// /// The order of array elements may be different than in Windows Settings. The primary screen is always at index 0. /// The array is not cached. Each time calls API EnumDisplayMonitors. /// public static screen[] all => _All(); [SkipLocalsInit] internal static unsafe screen[] _All(bool failed = false) { var a = stackalloc nint[256]; a[0] = 1; //array length a[1] = Api.MonitorFromWindow(default, SODefault.Primary); //fast [UnmanagedCallersOnly] static int _Enum(nint hmon, nint hdc, RECT* r, nint* a) { if (hmon != a[1]) a[++a[0]] = hmon; return 1; }; if (!Api.EnumDisplayMonitors(default, default, &_Enum, a)) { if (failed) throw new AuException("EnumDisplayMonitors failed"); Debug_.Print("EnumDisplayMonitors"); //in certain conditions EDM fails. // Where failed, it was used in incorrect code, maybe near stack overflow. // Anyway, then call it in other thread, then works. //print.it(lastError.message); //0 return Task.Run(() => _All(true)).GetAwaiter().GetResult(); } nint n = a[0]; var r = new screen[n]; for (int i = 0; i < n;) r[i++] = new(a[i]); return r; } /// /// Gets screen at the specified index of the array. /// /// 0-based screen index. Index 0 is the primary screen. If index too big, gets the primary screen. /// Create variable with that later will get screen handle. /// Negative index. public static screen index(int index, bool lazy = false) { if (index < 0) throw new ArgumentOutOfRangeException(); if (lazy) return new screen(() => screen.index(index)); if (index > 0) { var a = _All(); if (index < a.Length) return a[index]; //print.warning("Invalid screen index."); } return primary; } //We don't use a cached array that is updated like in Screen class code, eg on SystemEvents.DisplaySettingsChanging (wm_displaychanged). // Then index functions are faster, but less reliable when changing display settings, because the array is updated with a delay. // Now index functions are fast enough. Faster than EnumWindows which is used much more often. //At first there was an implicit conversion from int that called index()? I don't remember why removed, but probably for a good reason. /// /// Gets index of this screen in the array. /// /// /// Returns 0 (index of primary screen) if this variable is empty. Returns -1 if the screen handle is invalid; it can happen after changing display settings, but is rare. /// public int ScreenIndex { get { if (_h == default && _func == null) return 0; var h = _Handle(); var a = _All(); for (int i = 0; i < a.Length; i++) if (a[i]._h == h) return i; return -1; } } /// /// Gets screen rectangle and other info. /// /// /// Tuple containing: ///
rect - screen rectangle. ///
workArea - work area rectangle. ///
isPrimary - true if it is the primary screen. ///
isAlive - false if the screen handle is invalid; then the function gets info of the primary screen. ///
/// /// If this variable holds a callback function, this function calls it to get screen handle. See also . /// public unsafe (RECT rect, RECT workArea, bool isPrimary, bool isAlive) Info { get { var h = _func != null ? _Handle() : _h; for (int i = h != default ? 0 : 1; i < 10; i++, h = primary._h) { //retry if fails if (Api.GetMonitorInfo(h, out var m)) //fast return (m.rcMonitor, m.rcWork, 0 != (m.dwFlags & 1), i == 0); } return default; } } /// /// Calls and returns rectangle of the screen or its work area. /// /// Get work area rectangle. public RECT GetRect(bool workArea = false) { var v = Info; return workArea ? v.workArea : v.rect; } /// /// Calls and returns screen rectangle. /// public RECT Rect => Info.rect; /// /// Calls and returns work area rectangle. /// public RECT WorkArea => Info.workArea; /// /// Gets DPI of this screen. /// Calls . /// public int Dpi => More.Dpi.OfScreen(_Handle()); //public int Dpi(bool supportWin81 = false) => More.Dpi.OfScreen(_Handle(), supportWin81); //no, rarely need, don't complicate everything. When need, can use Dpi.OfScreen. /// /// True if the screen handle is valid. /// /// /// Don't use with variables that hold a callback function. This function does not call it and returns false. /// public unsafe bool IsAlive => _h != default && Api.GetMonitorInfo(_h, out _); /// public override string ToString() => _h.ToString() + " " + Rect.ToString(); /// public override int GetHashCode() => (int)_Handle(); /// public override bool Equals(object obj) => obj is screen && Equals((screen)obj); /// public bool Equals(screen other) => other._Handle() == _Handle(); /// public static bool operator ==(screen a, screen b) => a.Equals(b); /// public static bool operator !=(screen a, screen b) => !a.Equals(b); //rejected. GetHashCode gets hmonitor but is undocumented. Rarely used. ///// Converts from . //public static implicit operator screen(Screen scrn) => new screen((IntPtr)scrn.GetHashCode()); ///// Converts to . Returns null if failed. //public static implicit operator Screen(screen scrn) { int h=(int)scrn._Handle(); return Screen.AllScreens.FirstOrDefault(o => o.GetHashCode() == h); /// /// Returns true if point p is in some screen. /// public static bool isInAnyScreen(POINT p) => Api.MonitorFromPoint(p, SODefault.Zero) != default; /// /// Returns true if rectangle r intersects with some screen. /// public static bool isInAnyScreen(RECT r) => Api.MonitorFromRect(r, SODefault.Zero) != default; /// /// Returns true if rectangle of window w intersects with some screen. /// public static bool isInAnyScreen(wnd w) => Api.MonitorFromWindow(w, SODefault.Zero) != default; /// /// Gets bounding rectangle of all screens. /// public static RECT virtualScreen => new(Api.GetSystemMetrics(Api.SM_XVIRTUALSCREEN), Api.GetSystemMetrics(Api.SM_YVIRTUALSCREEN), Api.GetSystemMetrics(Api.SM_CXVIRTUALSCREEN), Api.GetSystemMetrics(Api.SM_CYVIRTUALSCREEN)); //20 times faster, but maybe less reliable //public static RECT virtualScreen2 => wnd.getwnd.shellWindow.Rect; internal screen ThrowIfWithHandle_ => _h == default ? this : throw new ArgumentException("screen with Handle. Must be lazy or empty."); } } namespace Au.Types { /// /// Used with to specify what screen to return if the window/point/etc is not in a screen or if the window handle is invalid etc. /// public enum SODefault { /// Create empty variable. Zero, //MONITOR_DEFAULTTONULL /// The primary screen. Primary, //MONITOR_DEFAULTTOPRIMARY /// The nearest screen. If window handle is invalid - the primary screen. Nearest, //MONITOR_DEFAULTTONEAREST } } ================================================ FILE: Au/Other/script+.cs ================================================ using System.Text.Json; namespace Au.Types { /// /// . /// public enum SRole { /// /// The task runs as normal .exe program. /// It can be started from editor or not. It can run on computers where editor not installed. /// ExeProgram, /// /// The task runs in Au.Task-x64.exe or Au.Task-arm.exe process, started from editor. /// MiniProgram, /// /// The task runs in editor process. /// EditorExtension, } /// /// Flags for parameter exception. Defines what to do on unhandled exception. /// Default is Print, even if script.setup not called (with default compiler only). /// [Flags] public enum UExcept { /// /// Display exception info in output. /// Print = 1, /// /// Show dialog with exception info. /// If editor available, the dialog contains links to functions in the call stack. To close the dialog when a link clicked, add flag Print. /// Dialog = 2, } /// /// The default compiler adds this attribute to the assembly. /// [AttributeUsage(AttributeTargets.Assembly)] public sealed class PathInWorkspaceAttribute : Attribute { /// Path of main source file in workspace, like @"\Script1.cs" or @"\Folder1\Script1.cs". public readonly string Path; /// Full path of main source file. public readonly string FilePath; /// public PathInWorkspaceAttribute(string path, string filePath) { Path = path; FilePath = filePath; } } /// /// The default compiler adds this attribute to the main assembly if using non-default references (meta r or nuget). Allows to find them at run time. Only if role miniProgram (default) or editorExtension. /// [AttributeUsage(AttributeTargets.Assembly)] public sealed class RefPathsAttribute : Attribute { /// Dll paths separated with |. public readonly string Paths; /// Dll paths separated with |. public RefPathsAttribute(string paths) { Paths = paths; } } /// /// The default compiler adds this attribute to the main assembly if using NuGet packages with native dlls. Allows to find the dlls at run time. Only if role miniProgram (default) or editorExtension. /// [AttributeUsage(AttributeTargets.Assembly)] public sealed class NativePathsAttribute : Attribute { /// Dll paths separated with |. public readonly string Paths; /// Dll paths separated with |. public NativePathsAttribute(string paths) { Paths = paths; } } /// /// . /// [Flags] public enum ECommandState { /// Checked = 1, /// Disabled = 2, } /// /// For . /// public enum EGetIcon { /// /// Input is a file or folder in current workspace. Can be relative path in workspace (like @"\Folder\File.cs") or full path or filename. /// Output must be icon name, like "*Pack.Icon color". See . /// PathToIconName, /// /// Input is a file or folder in current workspace (see PathToIconName). /// Output must be icon XAML. /// PathToIconXaml, /// /// Input is icon name, like "*Pack.Icon color". See . /// Output must be icon XAML. /// IconNameToXaml, //PathToGdipBitmap, //IconNameToGdipBitmap, } /// /// See . /// /// File name, like "File.cs". /// Path in workspace, like @"\Folder\File.cs". /// File text; null if needText false or if failed to get text. If the file is open in editor, it's the editor text, else it's the saved text. /// /// File id. /// Full path. /// Path of the workspace folder. public record class EFileInfo(string name, string path, string text, EFileKind kind, uint id, string filePath, string workspace); #pragma warning disable CS1591 //Missing XML comment for publicly visible type or member /// /// See . /// public enum EFileKind { Script, Class, Other } #pragma warning restore CS1591 //Missing XML comment for publicly visible type or member } namespace Au.More { /// /// Contains compilation info passed to current preBuild/postBuild script. /// /// Full path of the output exe or dll file. /// Meta comment outputPath. /// Path of this C# code file in the workspace. /// Meta comment role. /// Meta comment optimize. /// Meta comment platform. /// true if the script used with meta preBuild, false if with postBuild. /// true when publishing. /// /// /// public record class PrePostBuild(string outputFile, string outputPath, string source, string role, bool optimize, string platform, bool preBuild, bool publish) { /// /// Gets compilation info passed to current preBuild/postBuild script. /// public static PrePostBuild Info { get; internal set; } /// [Obsolete("Use platform."), EditorBrowsable(EditorBrowsableState.Never)] public bool bit32 => platform == "x86"; } } ================================================ FILE: Au/Other/script.cs ================================================ namespace Au; /// /// Script task functions. Run, get properties, set options, etc. /// A script task is a running script, except if role editorExtension. Each script task is a separate process. /// /// public static class script { #region properties /// /// Gets the script name, like "Script123". /// /// /// If role miniProgram (default), returns the script file name without extension. /// Else returns , like "MainAssemblyName". /// public static string name { get => s_name ??= AppDomain.CurrentDomain.FriendlyName; //info: in framework 4 with ".exe", now without (now it is the entry assembly name) internal set { s_name = value; } } static string s_name; /// /// Gets the script role (miniProgram, exeProgram or editorExtension). /// public static SRole role { get; internal set; } /// /// Gets path of the caller source code file. /// /// [](xref:caller_info) /// /// public static string sourcePath([CallerFilePath] string f_ = null) => f_; /// /// Gets path of the main source code file of this program or of a library. /// /// Get path in the workspace, like @"\Script1.cs" or @"\Folder1\Script1.cs". /// An assembly compiled by LibreAutomate. If null, uses . /// null if failed. /// /// When compiling, LibreAutomate adds to the assembly. Then at run time this function gets its value. Returns null if compiled by some other compiler. /// /// public static string sourcePath(bool inWorkspace, Assembly asm = null) { asm ??= AssemblyUtil_.GetEntryAssembly(); if (asm?.GetCustomAttribute() is not { } a) return null; return inWorkspace ? a.Path : a.FilePath; } /// /// Gets workspace path of the main source code file of this program, like @"\Script1.cs" or @"\Folder1\Script1.cs". /// Calls . /// [EditorBrowsable(EditorBrowsableState.Never)] //replaced with sourcePath. Limited and unclear. public static string path => sourcePath(true); /// /// Returns true if this script task was started from editor with the Run button or menu command. /// Always false if role editorExtension. /// public static bool testing { get; internal set; } /// /// Returns true if the build configuration of the main assembly is Debug (default). Returns false if Release (optimize true). /// public static bool isDebug => s_debug ??= AssemblyUtil_.IsDebug(AssemblyUtil_.GetEntryAssembly()); static bool? s_debug; //note: GetEntryAssembly returns null in func called by host through coreclr_create_delegate. /// /// Returns true if running in WPF preview mode. /// public static bool isWpfPreview { get { if (role != SRole.MiniProgram) return false; var s = Environment.CommandLine; //return s.Contains(" WPF_PREVIEW ") && s.RxIsMatch(@" WPF_PREVIEW (-?\d+) (-?\d+)$"); //slower JIT return s.Contains(" WPF_PREVIEW ") && _IsWpfPreview(s); //[MethodImpl(MethodImplOptions.NoInlining)] static bool _IsWpfPreview(string s) => s.RxIsMatch(@" WPF_PREVIEW (-?\d+) (-?\d+)$"); //don't cache. It makes JIT slower. Now fast after JIT. } } #endregion #region AppModuleInit_, setup /// /// If role miniProgram or exeProgram, default compiler adds module initializer that calls this with auCompiler true. /// When compiling single-file exe with dotnet publish, adds module initializer that calls this with auCompiler false. /// If using other compiler, called from script.setup with auCompiler false. /// [EditorBrowsable(EditorBrowsableState.Never)] public static unsafe void AppModuleInit_(bool auCompiler) { if (s_appModuleInit) return; s_appModuleInit = true; process.thisProcessCultureIsInvariant = true; Cpp.Cpp_UEF(true); //2 ms. Loads the C++ dll. Api.SetErrorMode(Api.SEM_NOGPFAULTERRORBOX | Api.SEM_FAILCRITICALERRORS); //SEM_NOGPFAULTERRORBOX disables WER. See also the workaround below. //CONSIDER: add setup parameter enableWER. //SEM_FAILCRITICALERRORS disables some error message boxes, eg when removable media not found; MSDN recommends too. AppDomain.CurrentDomain.UnhandledException += _UnhandledException; AppDomain.CurrentDomain.ProcessExit += (_, _) => { Exiting_ = true; Cpp.Cpp_UEF(false); }; if (role == SRole.ExeProgram) { //set STA thread if Main without [MTAThread] if (Thread.CurrentThread.GetApartmentState() != ApartmentState.STA) { //speed: 150 mcs if (null == Assembly.GetEntryAssembly().EntryPoint.GetCustomAttribute()) { //1.5 ms process.ThisThreadSetComApartment_(ApartmentState.STA); //1.6 ms } } int pidEditor = 0; if (auCompiler) { MiniProgram_.ResolveNugetRuntimes_(AppContext.BaseDirectory); var cd = Environment.CurrentDirectory; const string c_ep = "\\Roslyn\\.exeProgram"; if (cd.Ends(c_ep, true)) { //started from editor Environment.CurrentDirectory = folders.ThisApp; var p = &SharedMemory_.Ptr->script; pidEditor = p->pidEditor; s_wndEditorMsg = (wnd)p->hwndMsg; s_idMainFile = p->idMainFile; if (0 != (p->flags & 2)) script.testing = true; if (0 != (p->flags & 4)) ScriptEditor.IsPortable = true; if (0 != (p->flags & 8)) s_wrPipeName = p->pipe; if (0 != (p->flags & 16)) MiniProgram_.RedirectConsole_(); folders.Editor = new(cd[..^c_ep.Length]); folders.Workspace = new(p->workspace); var hevent = Api.OpenEvent(Api.EVENT_MODIFY_STATE, false, "Au.event.exeProgram.1"); if (!Api.SetEvent(hevent)) Environment.Exit(4); Api.CloseHandle(hevent); } } Starting_(AppDomain.CurrentDomain.FriendlyName, pidEditor); } } static bool s_appModuleInit; static UExcept s_setupException = UExcept.Print; internal static Exception s_unhandledException; //for process.thisProcessExit internal static wnd s_wndEditorMsg; internal static bool Exiting_ { get; private set; } [DebuggerNonUserCode] static void _UnhandledException(object sender, UnhandledExceptionEventArgs u) { if (!u.IsTerminating) return; //never seen, but anyway Exiting_ = true; Cpp.Cpp_UEF(false); var e = (Exception)u.ExceptionObject; //probably non-Exception object is impossible in C# s_unhandledException = e; if (Debugger.IsAttached) return; if (s_setupException.Has(UExcept.Print)) print.it(e); if (s_setupException.Has(UExcept.Dialog)) { var text = e.ToStringWithoutStack(); var d = new dialog("Task failed", null, "Close", flags: DFlags.ExpandDown, expandedText: e.ToString()); if (ScriptEditor.Available) { var st = new StackTrace(e, true); var b = new StringBuilder(text + "\n"); for (int i = 0; i < st.FrameCount; i++) { var f = st.GetFrame(i); if (f.HasSource()) b.Append($"\n{f.GetMethod()?.Name} in {pathname.getName(f.GetFileName())}:{f.GetFileLineNumber()}"); } text = b.ToString(); d.HyperlinkClicked += e => { if (s_setupException.Has(UExcept.Print)) e.d.Send.Close(); var f = st.GetFrame(e.LinkHref.ToInt()); ScriptEditor.Open(f.GetFileName(), f.GetFileLineNumber(), Math.Max(0, f.GetFileColumnNumber() - 1)); }; } d.Text2(text); d.ShowDialog(); } //workaround for .NET bug: randomly changes error mode. // Usually 0x3 -> 0x8001 (removed SEM_NOGPFAULTERRORBOX), sometimes even 0x0. Usually never restores. // Then on unhandled exception starts werfault.exe (with "wait" cursor), and the process exits with 1 s delay, even if WER disabled. // Tested: same in a standard simplest .NET program. But less frequently. // Usually it happens while _AuxThread is starting, often in Cpp_UEF (now removed). Rarely if _AuxThread not used. // Several ms before or after the script code starts. // The bug is in several places in CLR code. // It sets error mode, then executes code without lock and exception handling, then restores (if no exception). // Why it does not use SetThreadErrorMode? Why it uses the obsolete flag SEM_NOOPENFILEERRORBOX? Why it does not check maybe SEM_FAILCRITICALERRORS is already set (as recommended in doc)? Why it does not | the new error mode flags with current flags? // Could move _AuxThread to the C++ dll (not very easy). // Or move most of the _AuxThread startup code to the main thread (making script startup slower). // But it just would make this less frequent. // Anyway, moved Cpp_UEF here. It's better to load the dll sync, not at a random time later. // Never mind. This workaround solves the biggest problem for this library. Maybe future .NET will fix it. Api.SetErrorMode(Api.SEM_NOGPFAULTERRORBOX | Api.SEM_FAILCRITICALERRORS); } [StructLayout(LayoutKind.Sequential, Size = 256 + 1024)] //note: this struct is in shared memory. Size must be same in all library versions. internal unsafe struct SharedMemoryData_ { public int flags; //1 not received (let editor wait), 2 testing, 4 isPortable, 8 has pipe public int pidEditor; public int hwndMsg; public uint idMainFile; int _pipeLen; fixed char _pipeData[64]; int _workspaceLen; fixed char _workspaceData[1024]; public string pipe { get { fixed (char* p = _pipeData) return new(p, 0, _pipeLen); } set { fixed (char* p = _pipeData) value.AsSpan().CopyTo(new Span(p, 64)); _pipeLen = value.Length; } } public string workspace { get { fixed (char* p = _workspaceData) return new(p, 0, _workspaceLen); } set { fixed (char* p = _workspaceData) value.AsSpan().CopyTo(new Span(p, 1024)); _workspaceLen = value.Length; } } } /// /// Adds various features to this script task (running script): tray icon, exit on Ctrl+Alt+Delete, etc. /// /// Add tray icon. See . /// End this process when computer is going to sleep or hibernate. /// /// End this process when the active desktop has been switched (PC locked, Ctrl+Alt+Delete, screen saver, etc, except UAC consent). /// Then to end this process you can use hotkeys Win+L (lock computer) and Ctrl+Alt+Delete. /// Most mouse, keyboard, clipboard and window functions don't work when other desktop is active. Many of them then throw exception, and the script would end anyway. /// /// Call with usePrint true. It makes etc useful when not debugging. /// What to do on unhandled exception (event ). /// /// If not 0, the script task will end when this key pressed. Will call . /// Example: exitKey: KKey.MediaStop. /// /// Recommended keys: media, volume, browser and applaunch keys. They work even when the process of the active window is admin (UAC) and this script isn't. In any case, the key does not work if somewhere used for a global hotkey, trigger, exitKey or pauseKey. Also the key does not work when at that time a modifier key is pressed by a script; it also can be dangerous because may generate a trigger or hotkey used by an app or OS. /// /// /// /// Let pause/resume when this key pressed. Default: ScrollLock (Fn+S, Fn+K or similar). /// If CapsLock, pauses when it is toggled (even if was toggled at startup) and resumes when untoggled. /// /// ScrollLock, CapsLock and NumLock are the most reliable. Other keys have the same problems as with exitKey. /// /// /// [](xref:caller_info). Don't use. Or use like f_: null to disable script editing via tray icon. /// Already called. /// /// Tip: in Options > Templates you can set default code for new scripts. /// /// If your program was compiled not in LibreAutomate, call this function (maybe with zero arguments) if you want the program behave like if it was compiled with LibreAutomate (invariant culture, STAThread, unhandled exception action). /// /// Does nothing if role editorExtension or if running in WPF preview mode. /// public static void setup(bool trayIcon = false, bool sleepExit = false, bool lockExit = false, bool debug = false, UExcept exception = UExcept.Print, KKey exitKey = 0, KKey pauseKey = KKey.ScrollLock, [CallerFilePath] string f_ = null) { if (role == SRole.EditorExtension || isWpfPreview) return; if (s_setupOnce) throw new InvalidOperationException("script.setup already called"); s_setupOnce = true; s_setupException = exception; if (!s_appModuleInit) AppModuleInit_(auCompiler: false); //if role miniProgram, called by MiniProgram_.Init; else if default compiler, the call is compiled into code; else called now. if (debug) DebugTraceListener.Setup(usePrint: true); //info: default false, because slow and rarely used. s_exitKey = exitKey; s_pauseKey = pauseKey; s_pauseSetupDone = true; if (sleepExit || lockExit || exitKey != 0 || pauseKey is not (0 or KKey.CapsLock)) { s_sleepExit = sleepExit; s_lockExit = lockExit; s_exitKey = exitKey; s_auxThread.QueueAPC(static () => { if (s_sleepExit) { if (osVersion.minWin8) { //if Modern Standby, need RegisterSuspendResumeNotification to receive WM_POWERBROADCAST. // The API and MS are unavailable on Win7. // The API supports window handle and callback. With handle less problems. var h1 = Api.RegisterSuspendResumeNotification(s_auxWnd.Handle, 0); process.thisProcessExit += _ => { Api.UnregisterSuspendResumeNotification(h1); }; } else { WndUtil.CreateWindowDWP_(messageOnly: false, t_eocWP = (w, m, wp, lp) => { if (m == Api.WM_POWERBROADCAST && wp == Api.PBT_APMSUSPEND) _SleepLockExit(true); return Api.DefWindowProc(w, m, wp, lp); }); //message-only windows don't receive WM_POWERBROADCAST, unless used RegisterSuspendResumeNotification } } if (s_lockExit) { new WinEventHook(EEvent.SYSTEM_DESKTOPSWITCH, 0, k => { if (miscInfo.isInputDesktop()) return; if (process.exists("consent.exe", ofThisSession: true)) return; //UAC k.hook.Dispose(); _SleepLockExit(false); }); //tested: on Win+L works immediately. OS switches desktop 2 times. At first briefly, then makes defaul again, then on key etc switches again to show password field. } if (s_exitKey != 0) { if (!_RegisterKey(s_exitKey, 16)) { long i1 = 0; timer.every(500, t => { if (_RegisterKey(s_exitKey, 16)) t.Stop(); else if (++i1 == 4) print.warning($"{name}: script.setup failed to register the exit key. Will retry.", -1); }); } } if (s_pauseKey is not (0 or KKey.CapsLock)) _PauseSetKey(); }); } if (trayIcon) _TrayIcon(f_: f_); } static bool s_setupOnce, s_sleepExit, s_lockExit; static KKey s_exitKey; [ThreadStatic] static WNDPROC t_eocWP; /// /// Ensures that multiple processes that call this function don't run simultaneously. Like C# lock keyword for threads. /// /// Mutex name. If another process called this function with this mutex name, this process cannot run, and this function calls Environment.Exit(3);. /// Milliseconds to wait until this process can run. No timeout if -1. /// Don't print "cannot run". /// Called if this process cannot run. /// This function already called. /// /// This function is useful when this script has role exeProgram and the compiled program is launched not from the script editor, because then the /*/ ifRunning /*/ property is ignored. /// /// public static void single(string mutex = "Au-mutex-script.single", int wait = 0, bool silent = false, Action ifCantRun = null) { //FUTURE: parameter bool endOther. Like meta ifRunning restart. var m = Api.CreateMutex(null, false, mutex ?? "Au-mutex-script.single"); //tested: don't need Api.SECURITY_ATTRIBUTES.ForLowIL if (default != Interlocked.CompareExchange(ref s_singleMutex, m, default)) { Api.CloseHandle(m); throw new InvalidOperationException(); } var r = Api.WaitForSingleObject(s_singleMutex, wait); if (r is not (0 or Api.WAIT_ABANDONED)) { ifCantRun?.Invoke(); if (!silent) print.it($"<>Note: script task {name}<> cannot run because a task is running."); Environment.Exit(3); } //never mind: should release mutex. // Cannot release in process exit event. It runs in another thread. // Cannot use UsingEndAction, because then caller code must be like 'using var single = script.single();'. //return new(() => Api.ReleaseMutex(s_singleMutex)); } static IntPtr s_singleMutex; /// /// Low-level version of . No Environment.Exit, no exception, no print. Just CreateMutex and WaitForSingleObject. /// /// false if another process owns the mutex. internal static bool TrySingle_(string mutex, int wait = 0) => Api.WaitForSingleObject(Api.CreateMutex(null, false, mutex), wait) is 0 or Api.WAIT_ABANDONED; //public static bool single(out nint mutexHandle, string mutexName, int wait = 0) => Api.WaitForSingleObject(mutexHandle = Api.CreateMutex(null, false, mutexName), wait) is 0 or Api.WAIT_ABANDONED; /// /// Adds standard tray icon. /// /// Delay, milliseconds. /// Called before showing the tray icon. Can set its properties and event handlers. /// Called before showing context menu. Can add menu items. Menu item actions must not block messages etc for long time; if need, run in other thread or process (). /// [](xref:caller_info). Don't use. Or set = null to disable script editing via the tray icon. /// /// Uses other thread. The init and menu actions run in that thread too. It dispatches messages, therefore they also can set timers (), create hidden windows, etc. Current thread does not have to dispatch messages. /// /// Does nothing if role editorExtension. /// /// /// How to change icon and tooltip. /// { t.Icon = icon.stock(StockIcon.HELP); t.Tooltip = "Example"; }); /// ]]> /// How to add menu items. /// { /// m["Example"] = o => { dialog.show("Example"); }; /// m["Run another script"] = o => { script.run("Example.cs"); }; /// }); /// ]]> /// /// public static void trayIcon(int delay = 500, Action init = null, Action menu = null, [CallerFilePath] string f_ = null) { if (role == SRole.EditorExtension) return; if (!s_appModuleInit) AppModuleInit_(auCompiler: false); _TrayIcon(delay, init, menu, f_); } static void _TrayIcon(int delay = 500, Action init = null, Action menu = null, [CallerFilePath] string f_ = null) { s_auxThread.QueueAPC(() => timer.after(delay, _Delayed)); void _Delayed(timer t_) { var ti = new trayIcon { Tooltip = script.name }; init?.Invoke(ti); ti.Icon ??= icon.trayIcon(); bool canEdit = f_ != null && ScriptEditor.Available; if (canEdit) ti.Click += _ => ScriptEditor.Open(f_); ti.RightClick += e => { var m = new popupMenu(); if (menu != null) { menu(ti, m); if (m.Last != null && !m.Last.IsSeparator) m.Separator(); } if (canEdit) m["Open script"] = _ => ScriptEditor.Open(f_); m["End task"] = _ => Environment.Exit(2); if (canEdit) m["End and open"] = _ => { ScriptEditor.Open(f_); Environment.Exit(2); }; m.Show(PMFlags.AlignCenterH | PMFlags.AlignRectBottomTop, /*excludeRect: ti.GetRect(out var r1) ? r1 : null,*/ owner: ti.Hwnd); }; ti.Visible = true; } } #endregion #region aux thread internal static unsafe void Starting_(string name, int pidEditor, bool preloaded = false) { s_name = name; s_auxThread = new(() => _AuxThread(pidEditor, preloaded)); //using CreateThread because need thread handle ASAP } static NativeThread_ s_auxThread; /// /// Gets the aux thread object. Auto-creates (starts thread and does not wait) if used in an app that does not call Starting_ at startup (compiled not by LA). /// Thread-safe. /// internal static NativeThread_ GetAuxThread_() { if (s_auxThread != null) return s_auxThread; Debug.Assert(role != SRole.MiniProgram); lock ("s_auxThread") { return s_auxThread ??= new(() => _AuxThread(0, false)); } } //Auxiliary thread for various tasks: // Exit when editor process terminated or crashed. // Terminate script processes in a less brutal way. // Tray icon. // script.setup(sleepExit, lockExit) // Cpp_InactiveWindowWorkaround for miniProgram. // Can be used for various triggers. // Etc. static unsafe void _AuxThread(int pidEditor, bool preloaded) { Thread.CurrentThread.Name = "Au.Aux"; WndUtil.UacEnableMessages(Api.WM_COPYDATA, Api.WM_USER, Api.WM_CLOSE, c_msg_IconImageCache_ClearAll); WndUtil.RegisterWindowClass(c_auxWndClassName, _AuxWndProc); s_auxWnd = WndUtil.CreateMessageOnlyWindow(c_auxWndClassName, Api.GetCurrentProcessId().ToS()); _MessageLoop(pidEditor, preloaded); [MethodImpl(MethodImplOptions.NoInlining)] //need fast JIT of the main func, to make s_auxWnd available ASAP static void _MessageLoop(int pidEditor, bool preloaded) { //pidEditor 0 if exeProgram started not from editor var hp = pidEditor == 0 ? default : (IntPtr)Handle_.OpenProcess(pidEditor, Api.SYNCHRONIZE); //Cpp.Cpp_UEF(true); //moved to AppModuleInit_ if (preloaded) Cpp.Cpp_InactiveWindowWorkaround(true); NativeThread_.OfThisThread.ThreadInited(); int nh = hp == default ? 0 : 1; for (; ; ) { var k = Api.MsgWaitForMultipleObjectsEx(nh, &hp, -1, Api.QS_ALLINPUT, Api.MWMO_ALERTABLE | Api.MWMO_INPUTAVAILABLE); if (k == nh) { if (!wait.doEvents()) break; } else if (k == 0) { //editor process terminated or crashed _AuxExit(); } else if (k != Api.WAIT_IO_COMPLETION) { Debug_.Print(k); break; } } } } /// /// Class name of the auxiliary message-only window. /// internal const string c_auxWndClassName = "Au.Task.m3gVxcTJN02pDrHiQ00aSQ"; static unsafe nint _AuxWndProc(wnd w, int message, nint wp, nint lp) { switch (message) { //case Api.WM_COPYDATA: // return 0; //case Api.WM_USER: // return 0; case Api.WM_POWERBROADCAST: if (s_sleepExit && osVersion.minWin8 && wp == Api.PBT_APMSUSPEND) _SleepLockExit(true); break; case Api.WM_HOTKEY: if (wp is >= 0 and <= 15) { s_paused ^= true; } else if (wp is >= 16 and <= 31) { Environment.Exit(2); } break; case c_msg_IconImageCache_ClearAll: IconImageCache.ClearAll_(); break; case Api.WM_SETTEXT when wp != 0: try { string s = new((char*)lp); if (wp == c_msg_wmsettext_UpdateEnvVar) { //sent from LA by EnvVarUpdater on WM_SETTINGCHANGE var csv = csvTable.parse(s); foreach (var a in csv.Rows) Environment.SetEnvironmentVariable(a[0], a[1]); } } catch (Exception e1) { Debug_.Print(e1); } return 0; } var R = Api.DefWindowProc(w, message, wp, lp); if (message == Api.WM_DESTROY) _AuxExit(); return R; } internal const int c_msg_IconImageCache_ClearAll = Api.WM_USER + 5; internal const int c_msg_wmsettext_UpdateEnvVar = -100; static void _AuxExit() { Environment.Exit(1); //same speed //process.thisProcessExitInvoke(); //Api.ExitProcess(1); } /// /// Gets the message-only window of the aux thread. /// Waits if still not created. /// internal static wnd AuxWnd_ { get { if (s_auxWnd.Is0) { GetAuxThread_(); while (s_auxWnd.Is0) { Debug_.Print("waiting for s_auxWnd"); Thread.Sleep(12); } } return s_auxWnd; } } static wnd s_auxWnd; #endregion #region run /// /// Starts executing a script. Does not wait. /// /// Script name like "Script5.cs", or path like @"\Folder\Script5.cs". /// Command line arguments. In the script it will be variable args. Should not contain '\0' characters. /// /// Native process id of the task process. /// Returns -1 if failed, for example if the script contains errors or cannot run second task instance. /// Returns 0 if task start is deferred because the script is running (ifRunning wait/wait_restart). /// If role editorExtension, waits until the script ends, then returns 0. /// /// Script file not found. /// Script editor not running. public static int run([ParamString(PSFormat.CodeFile)] string script, params string[] args) => _Run(0, script, args, out _); /// /// Starts executing a script and waits until the task ends. /// /// The exit code of the task process. See . /// Script file not found. /// Failed to start script task. For example: the script contains errors; cannot start second task instance; script editor not running. /// public static int runWait([ParamString(PSFormat.CodeFile)] string script, params string[] args) => _Run(1, script, args, out _); /// /// Starts executing a script, waits until the task ends and then gets text. /// /// Receives text. /// The exit code of the task process. See . /// Script file not found. /// Failed to start script task. For example: the script contains errors; cannot start second task instance; script editor not running. /// public static int runWait(out string results, [ParamString(PSFormat.CodeFile)] string script, params string[] args) => _Run(3, script, args, out results); /// /// Starts executing a script, waits until the task ends and gets text in real time. /// /// Receives output whenever the task calls it. /// The exit code of the task process. See . /// Script file not found. /// Failed to start script task. For example: the script contains errors; cannot start second task instance; script editor not running. /// public static int runWait(Action results, [ParamString(PSFormat.CodeFile)] string script, params string[] args) => _Run(3, script, args, out _, results); //mode flags: 1 - wait, 3 - wait and get script.writeResult output, 4 restarting static int _Run(int mode, string script, string[] args, out string resultS, Action resultA = null) { resultS = null; var w = ScriptEditor.WndMsg_; if (w.Is0) throw new AuException("Editor process not found."); //CONSIDER: run editor program, if installed bool wait = 0 != (mode & 1), needResult = 0 != (mode & 2); using var tr = new _TaskResults(); if (needResult && !tr.Init()) throw new AuException("*get task results"); var data = Serializer_.Serialize(script, args, tr.pipeName); int pid = (int)WndCopyData.Send(w, 100, data, mode); if (pid == 0) pid--; //RunResult_.failed switch ((RunResult_)pid) { case RunResult_.failed: return !wait ? -1 : throw new AuException("*start task"); case RunResult_.notFound: throw new FileNotFoundException($"Script '{script}' not found."); case RunResult_.deferred: //possible only if !wait case RunResult_.editorThread: //the script ran sync and already returned return 0; } if (wait) { using var hProcess = WaitHandle_.FromProcessId(pid, Api.SYNCHRONIZE | Api.PROCESS_QUERY_LIMITED_INFORMATION); if (hProcess == null) throw new AuException("*wait for task"); if (!needResult) hProcess.WaitOne(-1); else if (!tr.WaitAndRead(hProcess, resultA)) throw new AuException("*get task result"); else if (resultA == null) resultS = tr.ResultString; if (!Api.GetExitCodeProcess(hProcess.SafeWaitHandle.DangerousGetHandle(), out pid)) pid = int.MinValue; } return pid; } //Called from editor's CommandLine. Almost same as _Run. Does not throw. internal static int RunCL_(wnd w, int mode, string script, string[] args, Action resultA, int schedPid = 0) { bool wait = 0 != (mode & 1), needResult = 0 != (mode & 2); using var tr = new _TaskResults(); if (needResult && !tr.Init()) return (int)RunResult_.cannotGetResult; var data = Serializer_.Serialize(script, args, tr.pipeName, schedPid); int taskProcessId = (int)WndCopyData.Send(w, 101, data, mode); if (taskProcessId == 0) taskProcessId--; //RunResult_.failed switch ((RunResult_)taskProcessId) { case RunResult_.failed: case RunResult_.notFound: return taskProcessId; case RunResult_.deferred: //possible only if !wait case RunResult_.editorThread: //the script ran sync and already returned. Ignore needResult, as it it auto-detected, not explicitly specified. return 0; } if (wait) { using var hProcess = WaitHandle_.FromProcessId(taskProcessId, Api.SYNCHRONIZE | Api.PROCESS_QUERY_LIMITED_INFORMATION); if (hProcess == null) return (int)RunResult_.cannotWait; if (!needResult) hProcess.WaitOne(-1); else if (!tr.WaitAndRead(hProcess, resultA)) return (int)RunResult_.cannotWaitGetResult; if (!Api.GetExitCodeProcess(hProcess.SafeWaitHandle.DangerousGetHandle(), out taskProcessId)) taskProcessId = int.MinValue; } return taskProcessId; } internal enum RunResult_ { //errors returned by sendmessage(wm_copydata) failed = -1, //script contains errors, or cannot run because of ifRunning, or sendmessage(wm_copydata) failed notFound = -2, //script not found deferred = -3, //script cannot run now, but will run later if don't need to wait. If need to wait, in such case cannot be deferred (then failed). editorThread = -4, //role editorExtension //other errors noEditor = -5, cannotWait = -6, cannotGetResult = -7, cannotWaitGetResult = -8, } unsafe struct _TaskResults : IDisposable { Handle_ _hPipe; public string pipeName; string _s; StringBuilder _sb; public bool Init() { var tid = Api.GetCurrentThreadId(); pipeName = @"\\.\pipe\Au.CL-" + tid.ToString(); //will send this string to the task _hPipe = Api.CreateNamedPipe(pipeName, Api.PIPE_ACCESS_INBOUND | Api.FILE_FLAG_OVERLAPPED, //use async pipe because also need to wait for task process exit Api.PIPE_TYPE_MESSAGE | Api.PIPE_READMODE_MESSAGE | Api.PIPE_REJECT_REMOTE_CLIENTS, 1, 0, 0, 0, Api.SECURITY_ATTRIBUTES.ForPipes); return !_hPipe.Is0; } public bool WaitAndRead(WaitHandle hProcess, Action results) { bool R = false; char* b = null; const int bLen = 7900; var ev = new ManualResetEvent(false); try { var ha = new WaitHandle[2] { ev, hProcess }; for (bool useSB = false; ; useSB = results == null) { var o = new Api.OVERLAPPED { hEvent = ev.SafeWaitHandle.DangerousGetHandle() }; if (!Api.ConnectNamedPipe(_hPipe, &o)) { int e = lastError.code; if (e != Api.ERROR_PIPE_CONNECTED) { if (e != Api.ERROR_IO_PENDING) break; int wr = WaitHandle.WaitAny(ha); if (wr != 0) { Api.CancelIo(_hPipe); R = true; break; } //task ended if (!Api.GetOverlappedResult(_hPipe, ref o, out _, false)) { Api.DisconnectNamedPipe(_hPipe); break; } } } if (b == null) b = (char*)MemoryUtil.Alloc(bLen); bool readOK; while (((readOK = Api.ReadFile(_hPipe, b, bLen, out int n, null)) || (lastError.code == Api.ERROR_MORE_DATA)) && n > 0) { n /= 2; if (!readOK) useSB = true; if (useSB) { //rare _sb ??= new StringBuilder(bLen); if (results == null && _s != null) _sb.Append(_s); _s = null; _sb.Append(b, n); } else { _s = new string(b, 0, n); } if (readOK) { if (results != null) { results(ResultString); _sb?.Clear(); } break; } //note: MSDN says must use OVERLAPPED with ReadFile too, but works without it. } Api.DisconnectNamedPipe(_hPipe); if (!readOK) break; } } finally { ev.Dispose(); MemoryUtil.Free(b); } return R; } public string ResultString => _s ?? _sb?.ToString(); public void Dispose() => _hPipe.Dispose(); }; /// /// Writes a string result for the task that called or to run this task, or for the program that started this task using command line like "Au.Editor.exe *Script5.cs". /// /// false if this task was not started in such a way. Or if failed to write, except when s is null/"". /// A string. This function does not append newline characters. /// /// can read the string in real time. /// gets all strings joined when the task ends. /// The program that started this task using command line like "Au.Editor.exe *Script5.cs" can read the string from the redirected standard output in real time, or the string is displayed to its console in real time. The string encoding is UTF-8; if you use a .bat file or cmd.exe and want to get correct Unicode text, execute this before, to change console code page to UTF-8: chcp 65001. /// /// Does not work if script role is editorExtension. /// #if true public static unsafe bool writeResult(string s) { if (s_wrPipeName == null) return false; if (s.NE()) return true; if (Api.WaitNamedPipe(s_wrPipeName, 3000)) { //15 mcs using var pipe = Api.CreateFile(s_wrPipeName, Api.GENERIC_WRITE, 0, Api.OPEN_EXISTING, 0); //7 mcs if (!pipe.Is0) { fixed (char* p = s) if (Api.WriteFile(pipe, p, s.Length * 2, out _)) return true; //17 mcs } } Debug_.PrintNativeError(); return false; //TODO3: optimize. Eg the app may override TextWriter.Write(char) and call this on each char in a string etc. // Now 40 mcs. Console.Write(char) 20 mcs. } internal static string s_wrPipeName; #else //does not work public static unsafe bool writeResult(string s) { if (s_wrPipeName == null) return false; if (s.NE()) return true; if (s_wrPipe.Is0) { if (Api.WaitNamedPipe(s_wrPipeName, 3000)) { //15 mcs lock (s_wrPipeName) { if (s_wrPipe.Is0) { s_wrPipe = Api.CreateFile(s_wrPipeName, Api.GENERIC_WRITE, 0, default, Api.OPEN_EXISTING, 0); } } } Debug_.PrintNativeError_(s_wrPipe.Is0); } if (!s_wrPipe.Is0) { fixed (char* p = s) if (Api.WriteFile(s_wrPipe, p, s.Length * 2, out _)) return true; //17 mcs else Debug_.PrintNativeError_(); //No process is on the other end of the pipe (0xE9) } return false; } static string s_wrPipeName; static Handle_ s_wrPipe; #endif /// /// Starts this script or program again. /// /// Command line arguments. Should not contain '\0' characters. /// /// Native process id of the new process. Returns -1 if failed. /// /// Script file not found. /// This script has role editorExtension. /// /// Does not end this process. The new process runs simultaneously, like with /*/ ifRunning run; /*/. Let this process exit as it wants, for example return from the main script code. /// /// If this process was started by LibreAutomate, the new process will be started by LibreAutomate too. Else this function simply starts a new instance of this program. /// public static int restart(params string[] args) { if (s_idMainFile != 0) return _Run(4, $":{s_idMainFile}", args, out _); if (role != SRole.ExeProgram) throw new InvalidOperationException(); //editorExtension var ps = new ProcessStarter_(process.thisExePath, StringUtil.CommandLineFromArray(args), null, rawExe: true); try { return ps.Start(inheritUiaccess: true).pid; } catch (Exception e1) { print.warning(e1); return -1; } } internal static uint s_idMainFile; /// /// Starts executing a script in child session running in picture-in-picture (PiP) window. Does not wait. /// /// Script name like "Script5.cs", or path like @"\Folder\Script5.cs". /// Command line arguments. In the script it will be variable args. Should not contain '\0' characters. /// Script file not found. /// Script editor not running (in this session). No exception if cannot run the script in PiP session. public static void runInPip([ParamString(PSFormat.CodeFile)] string script, params string[] args) { var w = ScriptEditor.WndMsg_; if (w.Is0) throw new AuException("Editor process not found."); //CONSIDER: run editor program, if installed var data = Serializer_.Serialize(script, args); RunResult_ r = (RunResult_)WndCopyData.Send(w, 102, data); switch (r) { //case RunResult_.failed: // throw new AuException("*start task in PiP"); case RunResult_.notFound: throw new FileNotFoundException($"Script '{script}' not found."); } } /// /// Returns true if this process is running in a child session (aka Picture-in-Picture). /// This function is an alias of . /// public static bool isInPip => miscInfo.isChildSession; #endregion #region end /// /// Ends this process. /// /// /// Calls . /// /// It executes process exit event handlers. Does not execute finally code blocks. Does not execute GC. /// public static void end() { Environment.Exit(0); } /// /// Ends another script process. /// /// Script process id, for example returned by . /// true if ended, false if failed, null if wasn't running. /// processId is 0 or id of this process. /// /// The script process can be started from editor or not. /// /// The process executes process exit event handlers. Does not execute finally code blocks and GC. /// /// Returns null if processId is invalid (probably because the script is already ended). Returns false if processId is valid but not of a script process (probably the script ended long time ago and the id is reused for another process). /// public static bool? end(int processId) { if (processId == 0 || processId == Api.GetCurrentProcessId()) throw new ArgumentException(); using var h = Handle_.OpenProcess(processId, Api.SYNCHRONIZE | Api.PROCESS_TERMINATE); //tested: UAC OK if (h.Is0) { if (lastError.code == Api.ERROR_INVALID_PARAMETER) return null; return false; } if (Api.WaitForSingleObject(h, 0) == 0) return null; var w = wait.until(-1d, () => wnd.findFast(processId.ToS(), c_auxWndClassName, messageOnly: true)); if (w.Is0) return 0 == Api.WaitForSingleObject(h, 1000); //don't terminate, maybe it's not a script process w.Post(Api.WM_CLOSE); if (0 == Api.WaitForSingleObject(h, 1000)) return true; if (!Api.TerminateProcess(h, -1)) return 0 == Api.WaitForSingleObject(h, 500); //TerminateProcess ERROR_ACCESS_DENIED when the process is ending Api.WaitForSingleObject(h, 500); //TerminateProcess is async. Usually the process ends after several ms. return true; } /// /// Ends all task processes of a script. /// /// /// Script file name (like "Script43.cs") or path in workspace (like @"\Folder\Script43.cs"), or full file path. /// If "", ends all other processes of this script. /// /// true if ended, false if failed (probably file not found), null if wasn't running. /// Editor process not found. /// /// Can end only script processes started from the editor. /// /// The process executes process exit event handlers. Does not execute finally code blocks and GC. /// public static bool? end([ParamString(PSFormat.CodeFile)] string name) { var w = ScriptEditor.WndMsg_; if (w.Is0) throw new AuException("Editor process not found."); int exceptPid = 0; if (name == "") (name, exceptPid) = ($":{s_idMainFile}", Api.GetCurrentProcessId()); int r = (int)WndCopyData.Send(w, 5, name, exceptPid); return r == 1 ? true : r == 2 ? null : false; } /// /// Returns true if the specified script task is running. /// /// Script file name (like "Script43.cs") or path in workspace (like @"\Folder\Script43.cs"), or full file path. public static bool isRunning([ParamString(PSFormat.CodeFile)] string name) { var w = ScriptEditor.WndMsg_; if (w.Is0) return false; return 0 != WndCopyData.Send(w, 6, name); } /// /// Returns true if the specified script task is running. /// /// Script process id, for example returned by . /// processId is 0 or id of this process. /// /// The script process can be started from editor or not. /// public static bool isRunning(int processId) { if (processId == 0 || processId == Api.GetCurrentProcessId()) throw new ArgumentException(); using var h = Handle_.OpenProcess(processId, Api.SYNCHRONIZE); if (h.Is0 || Api.WaitForSingleObject(h, 0) == 0) return false; var w1 = wait.until(-0.5, () => wnd.findFast(processId.ToS(), script.c_auxWndClassName, messageOnly: true)); return !w1.Is0; } #endregion #region debug, pause /// /// Attaches the LibreAutomate's debugger to this process, or waits for a debugger attached to this process. /// Does nothing if a debugger is already attached. /// /// Show dialog with process name and id. If false, attaches the LA debugger. /// /// When debugger is attached, this function returns and the script continues to run. The step mode begins when the script encounters one of: /// - breakpoint (set in the debugger's IDE). /// - exception. /// - clicked Pause button in IDE. /// - , etc. /// /// If showDialog is false and LibreAutomate is running, attaches the LA debugger. Cannot attach if it's busy (debugging). /// /// Some other programs that have a .NET debugger: /// - Visual Studio. It's the best, but huge (~10 GB). The community edition is free. Use menu Debug > Attach to process. /// - Visual Studio Code. It's much smaller. Free. /// - JetBrains Rider. /// /// If the script process is running as administrator, the debugger process must run as administrator too. /// /// When attaching an external debugger (Visual Studio etc), make sure it debugs .NET code, not native code etc. /// /// See also [](xref:debugger). /// [DebuggerStepThrough] public static void debug(bool showDialog = false) { if (Debugger.IsAttached) return; if (!showDialog && ScriptEditor.WndMsg_ is var w && !w.Is0) { if (0 != w.Send(Api.WM_USER, 30, process.thisProcessId)) { if (wait.until(-30, () => Debugger.IsAttached)) return; } end(); } var d = new dialog("Waiting for debugger to attach", $"{script.name}\nProcess: {process.thisExeName} {process.thisProcessId}", title: "Attach debugger"); d.InScreen(screen.ofMouse); d.ShowDialogNoWait(); wait.until(0, () => Debugger.IsAttached); d.Send.Close(); //note: don't add Debugger.Break(); in this func. It creates problems. } /// /// If was pressed the pause key, waits until the user presses it again. /// /// Text to display in the "Paused script" UI. /// Process Windows messages and other events while waiting. For example, windows of this thread can respond, and timers of this thread can run. /// /// The default pause key is ScrollLock (Fn+S, Fn+K or similar). To change, use parameter pauseKey. If script.setup not called, this function uses ScrollLock but does not pause when called the first time. /// /// A script can be paused only if it calls this function. Pausing at a random place would be dangerous and is not supported. Call this function in places where it is safe to pause, and where it makes sense, for example in a loop that preses keys or mouse buttons. To pause/resume, let the user press the pause key. /// /// If the pause key is CapsLock, waits if it is toggled, even if was toggled when this script started. /// /// /// /// public static void pause(string text = null, bool doEvents = false) { if (!s_pauseSetupDone) { //script.setup not called s_pauseSetupDone = true; s_pauseWhenUntoggled = keys.gui.isToggled(s_pauseKey = KKey.ScrollLock); return; } if (paused) { var s = $"Paused: {script.name}."; if (s_pauseKey != 0) s += $"\nKey: {s_pauseKey}."; if (!text.NE()) s += $"\n{text}"; using var icon = ImageUtil.LoadGdipBitmapFromXaml("", screen.primary.Dpi); using (osdText.showText(s, -1, new(y: ^10), icon, 0xffffff, 0x444444, showMode: OsdMode.WeakThread)) { wait.until(new(0) { Period = 2, DoEvents = doEvents }, () => !paused); } } } //FUTURE: UI to end task when paused. //CONSIDER: add option to save-restore mouse xy, active window, its state. //CONSIDER: auto call pause in key/mouse/etc functions if there are no pressed modifier keys and mouse buttons. /// /// If true, next call to will wait until false, or already is waiting. /// public static bool paused { get => s_paused || (_PauseIsLockKey && keys.gui.isToggled(s_pauseKey) != s_pauseWhenUntoggled); set { s_paused = value; } } //in aux thread static void _PauseSetKey() { if (_PauseIsLockKey) { s_pauseWhenUntoggled = keys.gui.isToggled(s_pauseKey); } else { if (!_RegisterKey(s_pauseKey, 0)) { //print.warning("script.setup failed to register the pause key. Will use ScrollLock.", -1); //s_pauseWhenUntoggled = keys.gui.isToggled(s_pauseKey = KKey.ScrollLock); long i1 = 0; timer.every(500, t => { if (_RegisterKey(s_pauseKey, 0)) t.Stop(); else if (++i1 == 4) print.warning($"{name}: script.setup failed to register the pause key. Will retry.", -1); }); } } } static KKey s_pauseKey; static bool s_paused, s_pauseSetupDone, s_pauseWhenUntoggled; static bool _PauseIsLockKey => s_pauseKey is KKey.ScrollLock or KKey.CapsLock or KKey.NumLock; #endregion #region util static void _SleepLockExit(bool sleep) { print.it($"<>Info: task {name}<> ended because of {(sleep ? "PC sleep" : "switched desktop")} at {DateTime.Now.ToShortTimeString()}."); Task.Run(() => Environment.Exit(2)); //why Task.Run: with RegisterSuspendResumeNotification does not work well in same thread. } static bool _RegisterKey(KKey key, int idBase) { return Api.RegisterHotKey(s_auxWnd, idBase, Api.MOD_NOREPEAT, key); //rejected: try to register all mod combinations. Else does not work if the script pressed a mod key at that time. // for (int i = 0; i < 16; i++) { // var k = key; // if (k == KKey.Pause && 0 != (i & Api.MOD_CONTROL)) k = KKey.Break; //Ctrl+Pause = Break // if (!Api.RegisterHotKey(s_auxWnd, idBase + i, (uint)i | Api.MOD_NOREPEAT, k)) { //#if !true // //print.it(i, k, s_auxWnd, lastError.message); // if (k == KKey.Pause && i == 8) continue; //Win+Pause opens something in Windows Settings // while (--i >= 0) Api.UnregisterHotKey(s_auxWnd, idBase + i); // return false; // //any failed to register hotkey would be dangerous, because the user-pressed key combined with script-pressed modifiers would invoke the hotkey in its owner app // //Not good. Many keys fail because a hotkey with some modifiers is registered. Eg Esc, arrows, Home. Many are registered by OS with mod Win. // // Maybe instead just print warning "Failed to register hotkey X. It can be dangerous... Consider using media keys etc instead.". Fail only if cannot register the key without modifiers. // // Or use LL hook. Then can detect script-pressed keys. But no, it's too heavy; maybe every script will have script.setup with that key. //#else // if (i == 0) return false; //#endif // } // } // return true; } #endregion } ================================================ FILE: Au/Resources/AssemblyInfo.cs ================================================ [assembly: AssemblyTitle("Au")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] //more in global2.cs // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("d3087fac-a12d-4365-a620-7574cd89b17f")] // Version information for an assembly consists of the following four values: // // Major Version // Minor Version // Build Number // Revision // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] //[assembly: AssemblyVersion("1.0.0.*")] //[assembly: AssemblyVersion("1.0.1.0")] //moved to global2.cs //rejected: auto increment. // Creates more problems and work than is useful. Eg after modifying this project always need to rebuild all exe projects, else fails to load this dll. // VS adds 20-300 to the revision at each build. Why not 1? Now ~ 23000. What happens when it becomes the max possible 0xffff? // VS does not auto-increment the build number when "1.0.*". Now 7288. Where it gets these values? And does not reset them when I change eg minor. //[assembly: AssemblyFileVersion("1.0.0.0")] //This can be used eg to find public/protected _Identifier. //However most warnings are about uint. We disable them in project Properties: 3001,3002,3003,3009. //[assembly: CLSCompliant(true)] //rejected. It isn't. #if IDE_LA [assembly: InternalsVisibleTo("Au.Editor")] [assembly: InternalsVisibleTo("Au.Controls")] #else [assembly: InternalsVisibleTo("Au.Editor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100095c6b7a0fe60fbe4a77e52dd10a09331ee3c3a7399aa9cc17db8a015647469a19784d5e33a2450a0a49c37bf17c0c3223674f64104eae649ba27c51a90c24989faec87d59217d7850efc8151109bbf9b027b7714fc01788317d2b991b2c2669836a7725e942f76607efde5cdacd8c497a45c5f9673fcf102fdbf92237a524a4")] [assembly: InternalsVisibleTo("Au.Controls, PublicKey=0024000004800000940000000602000000240000525341310004000001000100095c6b7a0fe60fbe4a77e52dd10a09331ee3c3a7399aa9cc17db8a015647469a19784d5e33a2450a0a49c37bf17c0c3223674f64104eae649ba27c51a90c24989faec87d59217d7850efc8151109bbf9b027b7714fc01788317d2b991b2c2669836a7725e942f76607efde5cdacd8c497a45c5f9673fcf102fdbf92237a524a4")] #endif [assembly: AssemblyMetadata("RepositoryUrl", "https://github.com/qgindi/LibreAutomate")] ================================================ FILE: Au/String/ExtString.cs ================================================ //FUTURE: now in intellisense RStr methods are mixed with that of string. Many of them are not useful. Maybe .NET 10 or later will add an attribute to clean it up. using System.Buffers; namespace Au.Types; /// /// Adds extension methods for . /// /// /// Some .NET methods use by default, while others use ordinal or invariant comparison. It is confusing (difficult to remember), dangerous (easy to make bugs), slower and rarely useful. /// Microsoft recommends to specify . See Best practices for comparing strings in .NET. /// This class adds ordinal comparison versions of these methods. Same or similar name, for example Ends for EndsWith. /// See also . /// /// This class also adds more methods. /// You also can find string functions in other classes of this library, including , , , , , , . /// public static unsafe partial class ExtString { /// /// Compares this and other string. Returns true if equal. /// /// This string. Can be null. /// Other string. Can be null. /// Case-insensitive. /// /// Uses ordinal comparison (does not depend on current culture/locale). /// /// /// /// /// public static bool Eq(this string t, string s, bool ignoreCase = false) { return ignoreCase ? string.Equals(t, s, StringComparison.OrdinalIgnoreCase) : string.Equals(t, s); } /// /// Compares this strings with multiple strings. /// Returns 1-based index of the matching string, or 0 if none. /// /// This string. Can be null. /// Case-insensitive. /// Other strings. Strings can be null. /// /// Uses ordinal comparison (does not depend on current culture/locale). /// public static int Eq(this string t, bool ignoreCase, params ReadOnlySpan strings) { for (int i = 0; i < strings.Length; i++) if (Eq(t, strings[i], ignoreCase)) return i + 1; return 0; } /// /// Compares part of this string with other string. Returns true if equal. /// /// This string. /// Offset in this string. If invalid, returns false. /// Other string. /// Case-insensitive. /// s is null. /// /// Uses ordinal comparison (does not depend on current culture/locale). /// /// /// /// public static bool Eq(this string t, int startIndex, RStr s, bool ignoreCase = false) { int nt = t.Length, ns = s.LengthThrowIfNull_(); if ((uint)startIndex > nt || ns > nt - startIndex) return false; var span = t.AsSpan(startIndex, ns); if (!ignoreCase) return span.SequenceEqual(s); return span.Equals(s, StringComparison.OrdinalIgnoreCase); //Faster than string.Compare[Ordinal]. //With Tables_.LowerCase similar speed. Depends on whether match. } /// /// Compares part of this string with multiple strings. /// Returns 1-based index of the matching string, or 0 if none. /// /// This string. /// Offset in this string. If invalid, returns false. /// Case-insensitive. /// Other strings. /// A string in strings is null. /// /// Uses ordinal comparison (does not depend on current culture/locale). /// public static int Eq(this string t, int startIndex, bool ignoreCase = false, params ReadOnlySpan strings) { for (int i = 0; i < strings.Length; i++) if (t.Eq(startIndex, strings[i], ignoreCase)) return i + 1; return 0; } /// /// Compares part of this string with other string. Returns true if equal. /// /// This string. /// Range of this string. Can return true only if its length == s.Length. If invalid, returns false. /// Other string. /// Case-insensitive. /// s is null. /// /// Uses ordinal comparison (does not depend on current culture/locale). /// /// /// /// /// public static bool Eq(this string t, Range range, RStr s, bool ignoreCase = false) { int nt = t.Length, ns = s.LengthThrowIfNull_(); int i = range.Start.GetOffset(nt), len = range.End.GetOffset(nt) - i; return ns == len && t.Eq(i, s, ignoreCase); } /// /// Returns true if the specified character is at the specified position in this string. /// /// This string. /// Offset in this string. If invalid, returns false. /// Character. public static bool Eq(this string t, int index, char c) { if ((uint)index >= t.Length) return false; return t[index] == c; } /// /// Compares this and other string ignoring case (case-insensitive). Returns true if equal. /// /// This string. Can be null. /// Other string. Can be null. /// /// Uses ordinal comparison (does not depend on current culture/locale). /// public static bool Eqi(this string t, string s) => string.Equals(t, s, StringComparison.OrdinalIgnoreCase); //rejected. Not so often used. //public static bool Eqi(this string t, int startIndex, string s) => Eq(t, startIndex, s, true); /// /// Compares end of this string with other string. Returns true if equal. /// /// This string. /// Other string. /// Case-insensitive. /// s is null. /// /// Uses ordinal comparison (does not depend on current culture/locale). /// public static bool Ends(this string t, RStr s, bool ignoreCase = false) { int nt = t.Length, ns = s.LengthThrowIfNull_(); if (ns > nt) return false; var span = t.AsSpan(nt - ns); if (!ignoreCase) return span.SequenceEqual(s); return span.Equals(s, StringComparison.OrdinalIgnoreCase); //faster than EndsWith } /// /// Compares end of this string with multiple strings. /// Returns 1-based index of the matching string, or 0 if none. /// /// This string. /// Case-insensitive. /// Other strings. /// A string in strings is null. /// /// Uses ordinal comparison (does not depend on current culture/locale). /// public static int Ends(this string t, bool ignoreCase, params ReadOnlySpan strings) { for (int i = 0; i < strings.Length; i++) if (Ends(t, strings[i], ignoreCase)) return i + 1; return 0; } /// /// Returns true if this string ends with the specified character. /// /// This string. /// Character. public static bool Ends(this string t, char c) { int i = t.Length - 1; return i >= 0 && t[i] == c; } /// /// Compares beginning of this string with other string. Returns true if equal. /// /// This string. /// Other string. /// Case-insensitive. /// s is null. /// /// Uses ordinal comparison (does not depend on current culture/locale). /// public static bool Starts(this string t, RStr s, bool ignoreCase = false) { int nt = t.Length, ns = s.LengthThrowIfNull_(); if (ns > nt) return false; var span = t.AsSpan(0, ns); if (!ignoreCase) return span.SequenceEqual(s); return span.Equals(s, StringComparison.OrdinalIgnoreCase); //faster than StartsWith } /// /// Compares beginning of this string with multiple strings. /// Returns 1-based index of the matching string, or 0 if none. /// /// This string. /// Case-insensitive. /// Other strings. /// A string in strings is null. /// /// Uses ordinal comparison (does not depend on current culture/locale). /// public static int Starts(this string t, bool ignoreCase, params ReadOnlySpan strings) { for (int i = 0; i < strings.Length; i++) if (Starts(t, strings[i], ignoreCase)) return i + 1; return 0; } /// /// Returns true if this string starts with the specified character. /// /// This string. /// Character. public static bool Starts(this string t, char c) { return t.Length > 0 && t[0] == c; } //Speed test results with text of length 5_260_070 and 'find' text "inheritdoc": //IndexOf(Ordinal) 6 ms (depends on 'find' text; can be much faster if starts with a rare character) //IndexOf(OrdinalIgnoreCase) 32 ms //FindStringOrdinal() 8 ms //FindStringOrdinal(true) 32 ms //Like("*" + x + "*") 10 ms //Like("*" + x + "*", true) 12 ms //RxIsMatch(LITERAL) 13 ms //RxIsMatch(LITERAL|CASELESS) 19 ms //Regex.Match(CultureInvariant) 4 ms (when no regex-special characters or if escaped) //Regex.Match(CultureInvariant|IgnoreCase) 9 ms //Find2(true) 10 ms //Could optimize the case-insensitive Find. // Either use table (like Like), or for very long strings use Regex. // But maybe then result would be different in some cases, not sure. // How if contains Unicode surrogates? // Bad: slower startup, because need to create table or JIT Regex. // Never mind. //public static int Find2(this string t, string s, bool ignoreCase = false) { // if (!ignoreCase) return t.IndexOf(s, StringComparison.Ordinal); // int n = t.Length - s.Length; // if (n >= 0) { // if (s.Length == 0) return 0; // var m = Tables_.LowerCase; // char first = m[s[0]]; // for (int i = 0; i <= n; i++) { // if (m[t[i]] == first) { // int j = 1; while (j < s.Length && m[t[i + j]] == m[s[j]]) j++; // if (j == s.Length) return i; // } // } // } // return -1; //} /// /// Finds substring in this string. Returns its 0-based index, or -1 if not found. /// /// This string. /// Substring to find. /// Case-insensitive. /// s is null. /// /// Uses ordinal comparison (does not depend on current culture/locale). /// public static int Find(this string t, string s, bool ignoreCase = false) { return t.IndexOf(s, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); } /// /// Finds substring in part of this string. Returns its 0-based index, or -1 if not found. /// /// This string. /// Substring to find. /// The search start index. /// Case-insensitive. /// s is null. /// Invalid startIndex. /// /// Uses ordinal comparison (does not depend on current culture/locale). /// public static int Find(this string t, string s, int startIndex, bool ignoreCase = false) { return t.IndexOf(s, startIndex, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); } /// /// Finds substring in part of this string. Returns its 0-based index, or -1 if not found. /// /// This string. /// Substring to find. /// The search range. /// Case-insensitive. /// s is null. /// Invalid range. /// /// Uses ordinal comparison (does not depend on current culture/locale). /// public static int Find(this string t, string s, Range range, bool ignoreCase = false) { var (start, count) = range.GetOffsetAndLength(t.Length); return t.IndexOf(s, start, count, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); } //CONSIDER: make public. /// /// Like , but with a predicate. /// /// Called for each found substring until returns true. Receives this string and start and end offsets of the substring. internal static int Find_(this string t, string s, Func also, bool ignoreCase = false) { for (int i = 0; ; i++) { i = t.Find(s, i, ignoreCase); if (i < 0) break; if (also(t, i, i + s.Length)) return i; } return -1; } /// /// Finds the first character specified in chars. Returns its index, or -1 if not found. /// /// This string. /// Characters. /// The search range. /// chars is null. /// Invalid range. [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] //functions with Range parameter are very slow until fully optimized public static int FindAny(this string t, string chars, Range? range = null) { var (start, len) = range.GetOffsetAndLength(t.Length); int r = t.AsSpan(start, len).IndexOfAny(chars); return r < 0 ? r : r + start; } /// /// Finds the first character not specified in chars. Returns its index, or -1 if not found. /// /// This string. /// Characters. /// The search range. /// chars is null. /// Invalid range. [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] public static int FindNot(this string t, string chars, Range? range = null) { var (start, len) = range.GetOffsetAndLength(t.Length); int r = t.AsSpan(start, len).IndexOfAnyExcept(chars); return r < 0 ? r : r + start; } /// /// Finds the last character specified in chars (searches right to left). Returns its index, or -1 if not found. /// /// This string. /// Characters. /// The search range. /// chars is null. /// Invalid range. [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] public static int FindLastAny(this string t, string chars, Range? range = null) { var (start, len) = range.GetOffsetAndLength(t.Length); int r = t.AsSpan(start, len).LastIndexOfAny(chars); return r < 0 ? r : r + start; } /// /// Finds the last character not specified in chars (searches right to left). Returns its index, or -1 if not found. /// /// This string. /// Characters. /// The search range. /// chars is null. /// Invalid range. [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] public static int FindLastNot(this string t, string chars, Range? range = null) { var (start, len) = range.GetOffsetAndLength(t.Length); int r = t.AsSpan(start, len).LastIndexOfAnyExcept(chars); return r < 0 ? r : r + start; } /// /// Removes specified characters from the start and end of this string. /// /// The result string. /// This string. /// Characters to remove. /// chars is null. [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] public static string Trim(this string t, string chars) { var span = t.AsSpan().Trim(chars); return span.Length == t.Length ? t : new string(span); } /// /// Removes specified characters from the start of this string. /// /// The result string. /// This string. /// Characters to remove. /// chars is null. [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] public static string TrimStart(this string t, string chars) { var span = t.AsSpan().TrimStart(chars); return span.Length == t.Length ? t : new string(span); } /// /// Removes specified characters from the end of this string. /// /// The result string. /// This string. /// Characters to remove. /// chars is null. [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] public static string TrimEnd(this string t, string chars) { var span = t.AsSpan().TrimEnd(chars); return span.Length == t.Length ? t : new string(span); } /// /// Finds whole word. Returns its 0-based index, or -1 if not found. /// /// This string. /// Substring to find. /// The search range. /// Case-insensitive. /// Additional word characters. For example "_". /// Function that returns true for word characters. If null, uses . /// s is null. /// Invalid range. /// /// If s starts with a word character, finds substring that is not preceded by a word character. /// If s ends with a word character, finds substring that is not followed by a word character. /// Word characters are those for which isWordChar or returns true plus those specified in otherWordChars. /// Uses ordinal comparison (does not depend on current culture/locale). /// For Unicode surrogates (2-char characters) calls and ignores isWordChar and otherWordChars. /// public static int FindWord(this string t, string s, Range? range = null, bool ignoreCase = false, string otherWordChars = null, Func isWordChar = null) { Not_.Null(s); var (start, end) = range.GetStartEnd(t.Length); int lens = s.Length; if (lens == 0) return 0; //like IndexOf and Find bool wordStart = _IsWordChar(s, 0, false), wordEnd = _IsWordChar(s, lens - 1, true); for (int i = start, iMax = end - lens; i <= iMax; i++) { i = t.IndexOf(s, i, end - i, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); if (i < 0) break; if (wordStart && i > 0 && _IsWordChar(t, i - 1, true)) continue; if (wordEnd && i < iMax && _IsWordChar(t, i + lens, false)) continue; return i; } return -1; bool _IsWordChar(string s, int i, bool expandLeft) { //CONSIDER: use Rune char c = s[i]; if (c >= '\uD800' && c <= '\uDFFF') { //Unicode surrogates if (expandLeft) { if (char.IsLowSurrogate(s[i])) return i > 0 && char.IsHighSurrogate(s[i - 1]) && char.IsLetterOrDigit(s, i - 1); } else { if (char.IsHighSurrogate(s[i])) return i < s.Length - 1 && char.IsLowSurrogate(s[i + 1]) && char.IsLetterOrDigit(s, i); } } else { if (isWordChar?.Invoke(c) ?? char.IsLetterOrDigit(c)) return true; if (otherWordChars?.Contains(c) ?? false) return true; } return false; } } /// /// Returns . Returns 0 if this string is null. /// /// This string. [DebuggerStepThrough] public static int Lenn(this string t) => t?.Length ?? 0; /// /// Returns true if this string is null or empty (""). /// /// This string. [DebuggerStepThrough] public static bool NE(this string t) => t == null || t.Length == 0; /// /// Returns this string, or null if it is "" or null. /// /// This string. [DebuggerStepThrough] internal static string NullIfEmpty_(this string t) => t.NE() ? null : t; //not public because probably too rarely used. #if !DEBUG /// /// This function can be used with foreach to split this string into substrings as start/end offsets. /// /// This string. /// Characters that delimit the substrings. Or one of constants. /// /// Part of this string to split. /// /// /// [EditorBrowsable(EditorBrowsableState.Never)] //obsolete. Use Split or Lines. They are faster, have "trim" option; the returned array is easier to use and not too expensive. For words use regex. public static SegParser Segments(this string t, string separators, SegFlags flags = 0, Range? range = null) { return new SegParser(t, separators, flags, range); } #endif /// /// Splits this string into substrings as start/end offsets. /// public static StartEnd[] SplitSE(this string t, Range range, char separator, StringSplitOptions flags = 0) { var (start, len) = range.GetOffsetAndLength(t.Length); var a = t.AsSpan(start, len).SplitSE(separator, flags); return _SplitOffset(a, start); } #if !DEBUG /// Alias of SplitSE. [EditorBrowsable(EditorBrowsableState.Never)] //renamed public static StartEnd[] Split(this string t, Range range, char separator, StringSplitOptions flags = 0) => SplitSE(t, range, separator, flags); #endif /// /// Splits this string into substrings as start/end offsets. /// public static StartEnd[] SplitSE(this string t, Range range, string separator, StringSplitOptions flags = 0) { var (start, len) = range.GetOffsetAndLength(t.Length); var a = t.AsSpan(start, len).SplitSE(separator, flags); return _SplitOffset(a, start); } #if !DEBUG /// Alias of SplitSE. [EditorBrowsable(EditorBrowsableState.Never)] //renamed public static StartEnd[] Split(this string t, Range range, string separator, StringSplitOptions flags = 0) => SplitSE(t, range, separator, flags); #endif /// /// Splits this string into substrings as start/end offsets. Can be used multiple separators. /// public static StartEnd[] SplitAnySE(this string t, Range range, RStr separators, StringSplitOptions flags = 0) { var (start, len) = range.GetOffsetAndLength(t.Length); var a = t.AsSpan(start, len).SplitAnySE(separators, flags); return _SplitOffset(a, start); } #if !DEBUG /// Alias of SplitAnySE. [EditorBrowsable(EditorBrowsableState.Never)] //renamed public static StartEnd[] Split(this string t, Range range, StringSplitOptions flags, RStr separators) => SplitAnySE(t, range, separators, flags); #endif /// /// Splits this string into substrings as start/end offsets. Can be used multiple separators. /// public static StartEnd[] SplitAnySE(this string t, Range range, ReadOnlySpan separators, StringSplitOptions flags = 0) { var (start, len) = range.GetOffsetAndLength(t.Length); var a = t.AsSpan(start, len).SplitAnySE(separators, flags); return _SplitOffset(a, start); } #if !DEBUG /// Alias of SplitAnySE. [EditorBrowsable(EditorBrowsableState.Never)] //renamed public static StartEnd[] Split(this string t, Range range, StringSplitOptions flags, ReadOnlySpan separators) => SplitAnySE(t, range, separators, flags); #endif /// /// Splits this string span into substrings as start/end offsets. /// public static StartEnd[] SplitSE(this RStr t, char separator, StringSplitOptions flags = 0) => _Split(t, flags, false, 1, separator).a1; #if !DEBUG /// Alias of SplitSE. [EditorBrowsable(EditorBrowsableState.Never)] //renamed public static StartEnd[] Split(this RStr t, char separator, StringSplitOptions flags = 0) => SplitSE(t, separator, flags); #endif /// /// Splits this string span into substrings as start/end offsets. /// public static StartEnd[] SplitSE(this RStr t, string separator, StringSplitOptions flags = 0) => _Split(t, flags, false, 2, sep23: separator).a1; #if !DEBUG /// Alias of SplitSE. [EditorBrowsable(EditorBrowsableState.Never)] //renamed public static StartEnd[] Split(this RStr t, string separator, StringSplitOptions flags = 0) => SplitSE(t, separator, flags); #endif /// /// Splits this string span into substrings as start/end offsets. Can be used multiple separators. /// public static StartEnd[] SplitAnySE(this RStr t, RStr separators, StringSplitOptions flags = 0) => _Split(t, flags, false, 3, sep23: separators).a1; #if !DEBUG /// Alias of SplitAnySE. [EditorBrowsable(EditorBrowsableState.Never)] //renamed public static StartEnd[] SplitAny(this RStr t, RStr separators, StringSplitOptions flags = 0) => SplitAnySE(t, separators, flags); #endif /// /// Splits this string span into substrings as start/end offsets. Can be used multiple separators. /// public static StartEnd[] SplitAnySE(this RStr t, ReadOnlySpan separators, StringSplitOptions flags = 0) => _Split(t, flags, false, 4, sep4: separators).a1; #if !DEBUG /// Alias of SplitAnySE. [EditorBrowsable(EditorBrowsableState.Never)] //renamed public static StartEnd[] SplitAny(this RStr t, ReadOnlySpan separators, StringSplitOptions flags = 0) => SplitAnySE(t, separators, flags); #endif #if !DEBUG //FUTURE: delete these etc. Hidden in v1.12 2025-05-17. /// /// Splits this string span into substrings. /// [EditorBrowsable(EditorBrowsableState.Never)] //C# 14 mess public static string[] SplitS(this RStr t, char separator, StringSplitOptions flags = 0) => _Split(t, flags, true, 1, separator).a2; /// /// Splits this string span into substrings. /// [EditorBrowsable(EditorBrowsableState.Never)] //C# 14 mess public static string[] SplitS(this RStr t, string separator, StringSplitOptions flags = 0) => _Split(t, flags, true, 2, sep23: separator).a2; /// /// Splits this string span into substrings. Can be used multiple separators. /// [EditorBrowsable(EditorBrowsableState.Never)] //C# 14 mess public static string[] SplitAnyS(this RStr t, RStr separators, StringSplitOptions flags = 0) => _Split(t, flags, true, 3, sep23: separators).a2; /// /// Splits this string span into substrings. Can be used multiple separators. /// [EditorBrowsable(EditorBrowsableState.Never)] //C# 14 mess public static string[] SplitAnyS(this RStr t, ReadOnlySpan separators, StringSplitOptions flags = 0) => _Split(t, flags, true, 4, sep4: separators).a2; #endif [SkipLocalsInit] static (StartEnd[] a1, string[] a2) _Split(RStr t, StringSplitOptions flags, bool retStr, int sep, char sep1 = default, RStr sep23 = default, ReadOnlySpan sep4 = default) { const int na = 100; Span a = stackalloc StartEnd[na]; Span a2 = MemoryMarshal.Cast(a); List list = null; for (int add = 0; ;) { int n = sep switch { 1 => t.Split(a2, sep1, flags), 2 => t.Split(a2, sep23, flags), 3 => t.SplitAny(a2, sep23, flags), _ => t.SplitAny(a2, sep4, flags) }; //print.it(n, na); if (add > 0) _Offset(a, n, add); if (n < na) { if (retStr) { string[] r; if (list != null) { r = new string[list.Count + n]; for (int i = 0; i < list.Count; i++) r[i] = t[list[i].Range].ToString(); for (int i = 0, j = list.Count; i < n; i++) r[j++] = t[a[i].Range].ToString(); } else { r = new string[n]; for (int i = 0; i < n; i++) r[i] = t[a[i].Range].ToString(); } return (null, r); } else { return (list == null ? a[..n].ToArray() : [.. list, .. a[..n]], null); } } int last = a[^1].start; t = t[last..]; add += last; (list ??= new(na * 2)).AddRange(a[..^1]); } static void _Offset(Span ar, int n, int add) { foreach (ref var v in ar) { v.start += add; v.end += add; } } } static StartEnd[] _SplitOffset(StartEnd[] a, int start) { if (start != 0) { for (int i = 0; i < a.Length; i++) { a[i].start += start; a[i].end += start; } } return a; } /// /// Splits this string into lines. /// /// Array containing lines as strings. Does not include the last empty line, unless preferMore true. /// This string. /// Don't need empty lines. /// Add 1 array element if the string ends with a line separator or its length is 0. /// If false (default), recognizes these newlines: "\r\n", "\n" and "\r". If true, also recognizes "\f", "\x0085", "\x2028" and "\x2029". /// public static string[] Lines(this string t, bool noEmpty = false, bool preferMore = false, bool rareNewlines = false) => _Lines(t, noEmpty, preferMore, rareNewlines, true).a2; /// /// Splits this string or a range in it into lines as start/end offsets. /// /// Array containing start/end offsets of lines in the string (not in the range). Does not include the last empty line, unless preferMore true. /// This string. /// Range of this string. Example: var a = s.Lines(..); //split entire string. /// Don't need empty lines. /// Add 1 array element if the string range ends with a line separator or its length is 0. /// If false (default), recognizes these newlines: "\r\n", "\n" and "\r". If true, also recognizes "\f", "\x0085", "\x2028" and "\x2029". /// public static StartEnd[] Lines(this string t, Range range, bool noEmpty = false, bool preferMore = false, bool rareNewlines = false) { var (start, len) = range.GetOffsetAndLength(t.Length); var a = t.AsSpan(start, len).Lines(noEmpty, preferMore, rareNewlines); return _SplitOffset(a, start); } /// /// Splits this string into lines as start/end offsets. /// /// Array containing start/end offsets of lines. Does not include the last empty line, unless preferMore true. /// This string. /// Don't need empty lines. /// Add 1 array element if the string span ends with a line separator or its length is 0. /// If false (default), recognizes these newlines: "\r\n", "\n" and "\r". If true, also recognizes "\f", "\x0085", "\x2028" and "\x2029". public static StartEnd[] Lines(this RStr t, bool noEmpty = false, bool preferMore = false, bool rareNewlines = false) => _Lines(t, noEmpty, preferMore, rareNewlines, false).a1; [SkipLocalsInit, MethodImpl(MethodImplOptions.AggressiveOptimization)] static (StartEnd[] a1, string[] a2) _Lines(RStr t, bool noEmpty, bool preferMore, bool rareNewlines, bool retStr) { using var f = new FastBuffer(); var newline = rareNewlines ? s_newlineAll : s_newlineRN; int n = 0; for (int pos = 0; ;) { if (pos == t.Length && !preferMore) break; if (n == f.n) f.More(preserve: true); var span = t[pos..]; int len = span.IndexOfAny(newline); if (len < 0) { f[n++] = new(pos, t.Length); break; } else { if (!(noEmpty && len == 0)) f[n++] = new(pos, pos + len); pos += len + 1; if (span[len] == '\r' && pos < t.Length && t[pos] == '\n') pos++; } } if (!retStr) return (new Span(f.p, n).ToArray(), null); var a = new string[n]; for (int i = 0; i < n; i++) a[i] = t[f[i].Range].ToString(); return (null, a); } /// /// Returns the number of lines. /// /// This string. /// Add 1 if the string ends with a line separator or its length is 0. /// Part of this string or null (default). /// If false (default), recognizes these newlines: "\r\n", "\n" and "\r". If true, also recognizes "\f", "\x0085", "\x2028" and "\x2029". /// /// public static int LineCount(this string t, bool preferMore = false, Range? range = null, bool rareNewlines = false) => LineCount(range is null ? t : t.AsSpan(range.Value), preferMore, rareNewlines); /// [MethodImpl(MethodImplOptions.AggressiveOptimization)] public static int LineCount(this RStr t, bool preferMore = false, bool rareNewlines = false) { var newline = rareNewlines ? s_newlineAll : s_newlineRN; int n = 0; for (int pos = 0; ;) { if (pos == t.Length) { if (preferMore) n++; break; } n++; var span = t[pos..]; int len = span.IndexOfAny(newline); if (len < 0) break; pos += len + 1; if (span[len] == '\r' && pos < t.Length && t[pos] == '\n') pos++; } return n; } static readonly SearchValues s_newlineRN = SearchValues.Create(['\r', '\n']), s_newlineAll = SearchValues.Create(['\r', '\n', '\f', '\x85', '\x2028', '\x2029']); /// /// Converts this string to lower case. /// /// The result string. /// This string. /// /// Calls . /// public static string Lower(this string t) => t.ToLowerInvariant(); /// /// Converts this string to upper case. /// /// The result string. /// This string. /// /// Calls . /// public static string Upper(this string t) => t.ToUpperInvariant(); /// /// Converts this string or only the first character to upper case or all words to title case. /// /// The result string. /// This string. /// /// Culture, for example CultureInfo.CurrentCulture. If null (default) uses invariant culture. public static unsafe string Upper(this string t, SUpper how, CultureInfo culture = null) { if (how == SUpper.FirstChar) { if (t.Length == 0 || !char.IsLower(t, 0)) return t; var r = Rune.GetRuneAt(t, 0); r = culture != null ? Rune.ToUpper(r, culture) : Rune.ToUpperInvariant(r); int n = r.IsBmp ? 1 : 2; var m = new Span(&r, n); if (n == 2) r.EncodeToUtf16(m); return string.Concat(m, t.AsSpan(n)); } var ti = (culture ?? CultureInfo.InvariantCulture).TextInfo; t = t ?? throw new NullReferenceException(); if (how == SUpper.TitleCase) return ti.ToTitleCase(t); return ti.ToUpper(t); } #region ToNumber [MethodImpl(MethodImplOptions.AggressiveOptimization)] static long _ToInt(RStr t, int startIndex, out int numberEndIndex, bool toLong, STIFlags flags) { numberEndIndex = 0; int len = t.Length; if ((uint)startIndex > len) throw new ArgumentOutOfRangeException("startIndex"); int i = startIndex; char c; //skip spaces for (; ; i++) { if (i == len) return 0; c = t[i]; if (c > ' ') break; if (c == ' ') continue; if (c < '\t' || c > '\r') break; //\t \n \v \f \r } if (i > startIndex && 0 != (flags & STIFlags.DontSkipSpaces)) return 0; //skip arabic letter mark etc if (c >= '\x61C' && c is '\x61C' or '\x200E' or '\x200F') { if (++i == len) return 0; c = t[i]; } //skip -+ bool minus = false; if (c is '-' or '−' or '+') { if (++i == len) return 0; if (c != '+') minus = true; c = t[i]; } //is hex? bool isHex = false; switch (flags & (STIFlags.NoHex | STIFlags.IsHexWithout0x)) { case 0: if (c == '0' && i <= len - 3) if (isHex = t[i + 1] is 'x' or 'X' && t[i + 2] is (>= '0' and <= '9') or (>= 'A' and <= 'F') or (>= 'a' and <= 'f')) i += 2; break; case STIFlags.IsHexWithout0x: isHex = true; break; } //skip '0' int i0 = i; while (i < len && t[i] == '0') i++; long R = 0; //result int nDigits = 0; if (isHex) { int nMaxDigits = toLong ? 16 : 8; for (; i < len; i++) { int k = _CharHexToDec(t[i]); if (k < 0) break; if (++nDigits > nMaxDigits) return 0; R = (R << 4) + k; } } else { //decimal or not a number int nMaxDigits = toLong ? 20 : 10; for (; i < len; i++) { int k = t[i] - '0'; if (k < 0 || k > 9) break; R = R * 10 + k; //is too long? if (++nDigits >= nMaxDigits) { if (nDigits > nMaxDigits) return 0; if (toLong) { if (t.Slice(i + 1 - nDigits).CompareTo("18446744073709551615", StringComparison.Ordinal) > 0) return 0; } else { if (R > uint.MaxValue) return 0; } } } } if (i == i0) return 0; //not a number numberEndIndex = i; return minus ? -R : R; } [MethodImpl(MethodImplOptions.AggressiveInlining)] static int _CharHexToDec(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'A' && c <= 'F') return c - ('A' - 10); if (c >= 'a' && c <= 'f') return c - ('a' - 10); return -1; } /// /// Converts part of this string to int number and gets the number end index. /// /// The number, or 0 if failed to convert. /// This string. Can be null. /// Offset in this string where to start parsing. /// Receives offset in this string where the number part ends. If fails to convert, receives 0. /// /// startIndex is less than 0 or greater than string length. /// /// Fails to convert when string is null, "", does not start with a number or the number is too big. /// /// Unlike and : /// - The number in string can be followed by more text, like "123text". /// - Has startIndex parameter that allows to get number from middle, like "text123text". /// - Gets the end of the number part. /// - No exception when cannot convert. /// - The number can be decimal (like "123") or hexadecimal (like "0x1A"); don't need separate flags for each style. /// - Does not depend on current culture. As minus sign recognizes '-' and '−'. /// - Faster. /// /// The number in string can start with ASCII whitespace (spaces, newlines, etc), like " 5". /// The number in string can be with "-" or "+", like "-5", but not like "- 5". /// Fails if the number is greater than +- (0xffffffff). /// The return value becomes negative if the number is greater than , for example "0xffffffff" is -1, but it becomes correct if assigned to uint (need cast). /// Does not support non-integer numbers; for example, for "3.5E4" returns 3 and sets numberEndIndex=startIndex+1. /// public static int ToInt(this string t, int startIndex, out int numberEndIndex, STIFlags flags = 0) { return (int)_ToInt(t, startIndex, out numberEndIndex, false, flags); } /// /// Converts part of this string to int number. /// /// /// public static int ToInt(this string t, int startIndex = 0, STIFlags flags = 0) { return (int)_ToInt(t, startIndex, out _, false, flags); } /// false if failed. /// Receives the result, or 0 if failed. /// /// public static bool ToInt(this string t, out int result, int startIndex, out int numberEndIndex, STIFlags flags = 0) { result = (int)_ToInt(t, startIndex, out numberEndIndex, false, flags); return numberEndIndex != 0; } /// false if failed. /// Receives the result, or 0 if failed. /// /// public static bool ToInt(this string t, out int result, int startIndex = 0, STIFlags flags = 0) => ToInt(t, out result, startIndex, out _, flags); /// /// Converts part of this string to uint number and gets the number end index. /// /// /// public static bool ToInt(this string t, out uint result, int startIndex, out int numberEndIndex, STIFlags flags = 0) { result = (uint)_ToInt(t, startIndex, out numberEndIndex, false, flags); return numberEndIndex != 0; } /// /// Converts part of this string to uint number. /// /// /// public static bool ToInt(this string t, out uint result, int startIndex = 0, STIFlags flags = 0) => ToInt(t, out result, startIndex, out _, flags); /// /// Converts part of this string to long number and gets the number end index. /// /// /// public static bool ToInt(this string t, out long result, int startIndex, out int numberEndIndex, STIFlags flags = 0) { result = _ToInt(t, startIndex, out numberEndIndex, true, flags); return numberEndIndex != 0; } /// /// Converts part of this string to long number. /// /// /// public static bool ToInt(this string t, out long result, int startIndex = 0, STIFlags flags = 0) => ToInt(t, out result, startIndex, out _, flags); /// /// Converts part of this string to ulong number and gets the number end index. /// /// /// public static bool ToInt(this string t, out ulong result, int startIndex, out int numberEndIndex, STIFlags flags = 0) { result = (ulong)_ToInt(t, startIndex, out numberEndIndex, true, flags); return numberEndIndex != 0; } /// /// Converts part of this string to ulong number. /// /// /// public static bool ToInt(this string t, out ulong result, int startIndex = 0, STIFlags flags = 0) => ToInt(t, out result, startIndex, out _, flags); //FUTURE: all `ToInt(this string...)` -> `ToInt(this RStr...)`. internal static int ToInt_(this RStr t, STIFlags flags = 0) => (int)_ToInt(t, 0, out _, false, flags); internal static bool ToInt_(this RStr t, out int result, out int numberEndIndex, STIFlags flags = 0) { result = (int)_ToInt(t, 0, out numberEndIndex, false, flags); return numberEndIndex != 0; } /// /// Converts this string or its part to double number. /// /// The number, or 0 if failed to convert. /// This string. Can be null. /// Part of this string or null (default). /// The permitted number format in the string. /// Invalid range. /// Invalid style. /// /// Calls with InvariantCulture. /// Fails if the string is null or "" or isn't a valid floating-point number. /// Examples of valid numbers: "12", " -12.3 ", ".12", "12.", "12E3", "12.3e-45", "1,234.5" (with style NumberStyles.Float | NumberStyles.AllowThousands). String like "2text" is invalid, unless range is 0..1. /// public static double ToNumber(this string t, Range? range = null, NumberStyles style = NumberStyles.Float) { ToNumber(t, out double r, range, style); return r; } /// false if failed. /// Receives the result, or 0 if failed. /// public static bool ToNumber(this string t, out double result, Range? range = null, NumberStyles style = NumberStyles.Float) { return double.TryParse(_NumSpan(t, range, out var ci), style, ci, out result); } /// /// Converts this string or its part to float number. /// /// /// Calls with InvariantCulture. /// /// public static bool ToNumber(this string t, out float result, Range? range = null, NumberStyles style = NumberStyles.Float) { return float.TryParse(_NumSpan(t, range, out var ci), style, ci, out result); } /// /// Converts this string or its part to int number. /// /// /// Calls with InvariantCulture. /// /// public static bool ToNumber(this string t, out int result, Range? range = null, NumberStyles style = NumberStyles.Integer) { return int.TryParse(_NumSpan(t, range, out var ci), style, ci, out result); //note: exception if NumberStyles.Integer | NumberStyles.AllowHexSpecifier. // Can parse either decimal or hex, not any. // Does not support "0x". With AllowHexSpecifier eg "11" is 17, but "0x11" is invalid. } /// /// Converts this string or its part to uint number. /// /// /// Calls with InvariantCulture. /// /// public static bool ToNumber(this string t, out uint result, Range? range = null, NumberStyles style = NumberStyles.Integer) { return uint.TryParse(_NumSpan(t, range, out var ci), style, ci, out result); } /// /// Converts this string or its part to long number. /// /// /// Calls with InvariantCulture. /// /// public static bool ToNumber(this string t, out long result, Range? range = null, NumberStyles style = NumberStyles.Integer) { return long.TryParse(_NumSpan(t, range, out var ci), style, ci, out result); } /// /// Converts this string or its part to ulong number. /// /// /// Calls . Uses if the string range contains only ASCII characters, else uses current culture. /// /// public static bool ToNumber(this string t, out ulong result, Range? range = null, NumberStyles style = NumberStyles.Integer) { return ulong.TryParse(_NumSpan(t, range, out var ci), style, ci, out result); } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] static RStr _NumSpan(string t, Range? range, out CultureInfo ci) { ci = CultureInfo.InvariantCulture; if (t == null) return default; var (start, len) = range.GetOffsetAndLength(t.Length); //Workaround for .NET 5 preview 7 bug: if current user culture is eg Norvegian or Lithuanian, // 'number to/from string' functions use '−' (Unicode minus), not '-' (ASCII hyphen), even if in Control Panel is ASCII hyphen. // Tested: no bug in .NET Core 3.1. //Also, in some cultures eg Arabic there are more chars. if (!t.AsSpan(start, len).IsAscii()) ci = CultureInfo.CurrentCulture; return t.AsSpan(start, len); } #endregion /// /// Inserts other string. /// /// The result string. /// This string. /// Offset in this string. Can be from end, like ^4. /// String to insert. /// Invalid startIndex. public static string Insert(this string t, Index startIndex, string s) { return t.Insert(startIndex.GetOffset(t.Length), s); } /// /// Replaces part of this string with other string. /// /// The result string. /// This string. /// Offset in this string. /// Count of characters to replace. /// The replacement string. /// Invalid startIndex or count. public static string ReplaceAt(this string t, int startIndex, int count, string s) { int i = startIndex; if (count == 0) return t.Insert(i, s); return string.Concat(t.AsSpan(0, i), s, t.AsSpan(i + count)); } /// /// Replaces part of this string with other string. /// /// The result string. /// This string. /// Part of this string to replace. /// The replacement string. /// Invalid range. public static string ReplaceAt(this string t, Range range, string s) { var (i, count) = range.GetOffsetAndLength(t.Length); return ReplaceAt(t, i, count, s); } /// /// Removes part of this string. /// /// The result string. /// This string. /// Part of this string to remove. /// Invalid ranget. public static string Remove(this string t, Range range) { var (i, count) = range.GetOffsetAndLength(t.Length); return t.Remove(i, count); } //rejected. Use [..^count]. ///// ///// Removes count characters from the end of this string. ///// ///// The result string. ///// This string. ///// Count of characters to remove. ///// //public static string RemoveSuffix(this string t, int count) => t[^count]; /// /// Removes suffix string from the end. /// /// The result string. Returns this string if does not end with suffix. /// This string. /// Substring to remove. /// Case-insensitive. /// suffix is null. public static string RemoveSuffix(this string t, string suffix, bool ignoreCase = false) { if (!t.Ends(suffix, ignoreCase)) return t; return t[..^suffix.Length]; } /// /// Removes suffix character from the end. /// /// The result string. Returns this string if does not end with suffix. /// This string. /// Character to remove. /// suffix is null. public static string RemoveSuffix(this string t, char suffix) { if (!t.Ends(suffix)) return t; return t[..^1]; } /// /// If this string is longer than limit, returns its substring 0 to limit-1 with appended '…' character. /// Else returns this string. /// /// This string. /// Maximal length of the result string. If less than 1, uses 1. /// Let "…" be in the middle. For example it is useful when the string is a file path, to avoid removing the filename. /// limit is lines, not characters. public static string Limit(this string t, int limit, bool middle = false, bool lines = false) { if (limit < 1) limit = 1; if (lines) { var a = t.AsSpan().Lines(); int k = a.Length; if (k > limit) { limit--; //for "…" line if (limit == 0) return t[a[0].Range] + "…"; if (middle) { if (limit == 1) return t[a[0].Range] + "\r\n…"; int half = limit - limit / 2; //if limit is odd number, prefer more lines at the start int half2 = a.Length - (limit - half); //if (half2 == a.Length - 1 && a[half2].Length == 0) return t[..a[half].end] + "\r\n…"; //rejected: if ends with newline, prefer more lines at the start than "\r\n…\r\n" at the end return t.ReplaceAt(a[half - 1].end..a[half2].start, "\r\n…\r\n"); } else { return t[..a[limit - 1].end] + "\r\n…"; } } } else if (t.Length > limit) { limit--; //for "…" if (middle) { int i = _Correct(t, limit / 2); int j = _Correct(t, t.Length - (limit - i), 1); return t.ReplaceAt(i..j, "…"); } else { limit = _Correct(t, limit); return t[..limit] + "…"; } //ensure not in the middle of a surrogate pair or \r\n static int _Correct(string s, int i, int d = -1) { if (i > 0 && i < s.Length) { char c = s[i - 1]; if ((c == '\r' && s[i] == '\n') || char.IsSurrogatePair(c, s[i])) i += d; } return i; } } return t; } /// /// Replaces unsafe characters with C# escape sequences. /// If the string contains these characters, replaces and returns new string. Else returns this string. /// /// This string. /// If the final string is longer than limit, get its substring 0 to limit-1 with appended '…' character. The enclosing "" are not counted. /// Enclose in "". /// /// Replaces these characters: '\\', '"', '\t', '\n', '\r' and all in range 0-31. /// [MethodImpl(MethodImplOptions.AggressiveOptimization)] public static string Escape(this string t, int limit = 0, bool quote = false) { int i, len = t.Length; if (len == 0) return quote ? "\"\"" : t; if (limit > 0) { if (len > limit) len = limit - 1; else limit = 0; } for (i = 0; i < len; i++) { var c = t[i]; if (c < ' ' || c == '\\' || c == '"') goto g1; //tested: Unicode line-break chars in most controls don't break lines, therefore don't need to escape } if (limit > 0) t = Limit(t, limit); if (quote) t = "\"" + t + "\""; return t; g1: using (new StringBuilder_(out var b, len + len / 16 + 100)) { if (quote) b.Append('"'); for (i = 0; i < len; i++) { var c = t[i]; if (c < ' ') { switch (c) { case '\t': b.Append("\\t"); break; case '\n': b.Append("\\n"); break; case '\r': b.Append("\\r"); break; case '\0': b.Append("\\0"); break; default: b.Append("\\u").Append(((ushort)c).ToString("x4")); break; } } else if (c == '\\') b.Append("\\\\"); else if (c == '"') b.Append("\\\""); else b.Append(c); if (limit > 0 && b.Length - (quote ? 1 : 0) >= len) break; } if (limit > 0) b.Append('…'); if (quote) b.Append('"'); return b.ToString(); } } /// /// Replaces C# escape sequences to characters in this string. /// /// false if the string contains an invalid or unsupported escape sequence. /// This string. /// Receives the result string. It is this string if there are no escape sequences or if failed. /// /// Supports all escape sequences of : \\, \", \t, \n, \r, \0, \uXXXX. /// Does not support \a, \b, \f, \v, \e, \', \xXXXX, \UXXXXXXXX. /// [MethodImpl(MethodImplOptions.AggressiveOptimization)] public static bool Unescape(this string t, out string result) { result = t; int i = t.IndexOf('\\'); if (i < 0) return true; using (new StringBuilder_(out var b, t.Length)) { b.Append(t, 0, i); for (; i < t.Length; i++) { char c = t[i]; if (c == '\\') { if (++i == t.Length) return false; switch (c = t[i]) { case '\\': case '"': break; case 't': c = '\t'; break; case 'n': c = '\n'; break; case 'r': c = '\r'; break; case '0': c = '\0'; break; //case 'a': c = '\a'; break; //case 'b': c = '\b'; break; //case 'f': c = '\f'; break; //case 'v': c = '\v'; break; //case 'e': c = '\e'; break; //also we don't support U and x case 'u': if (!_Uni(t, ++i, 4, out int u)) return false; c = (char)u; i += 3; break; default: return false; } } b.Append(c); } result = b.ToString(); return true; } static bool _Uni(string t, int i, int maxLen, out int R) { R = 0; int to = i + maxLen; if (to > t.Length) return false; for (; i < to; i++) { int k = _CharHexToDec(t[i]); if (k < 0) return false; R = (R << 4) + k; } return true; } } /// New string if replaced, else this string. Does not replace if the string does not contain escape sequences or if some escape sequences are invalid or unsupported. /// public static string Unescape(this string t) { Unescape(t, out t); return t; } /// /// Reverses this string, like "Abc" -> "cbA". /// /// The result string. /// /// Ignore char sequences such as Unicode surrogates and grapheme clusters. Faster, but if the string contains these sequences, the result string is incorrect. public static unsafe string ReverseString(this string t, bool raw) { if (t.Length < 2) return t; var r = new string('\0', t.Length); fixed (char* p = r) { if (raw || t.IsAscii()) { for (int i = 0, j = t.Length; i < t.Length; i++) { p[--j] = t[i]; } } else { var a = StringInfo.ParseCombiningCharacters(t); //speed: same as StringInfo.GetTextElementEnumerator+MoveNext+ElementIndex for (int gTo = t.Length, j = 0, i = a.Length; --i >= 0; gTo = a[i]) { for (int g = a[i]; g < gTo; g++) p[j++] = t[g]; } } } return r; //tested: string.Create slower. } /// /// Returns true if does not contain non-ASCII characters. /// public static bool IsAscii(this string t) => t.AsSpan().IsAscii(); //FUTURE: remove this and other functions that in C# 14+ are duplicate or useless. /// /// Returns true if does not contain non-ASCII characters. /// public static bool IsAscii(this RStr t) => !t.ContainsAnyExceptInRange((char)0, (char)127); /// /// Returns true if does not contain non-ASCII character bytes. /// public static bool IsAscii(this RByte t) => !t.ContainsAnyExceptInRange((byte)0, (byte)127); /// /// Returns true if null pointer. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsNull(this RStr t) => t == RStr.Empty; /// /// Returns true if null pointer. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsNull(this RByte t) => t == RByte.Empty; [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static int LengthThrowIfNull_(this RStr t) { int n = t.Length; if (n == 0 && t.IsNull()) throw new ArgumentNullException(); return n; } /// /// Returns true if equals to string s, case-sensitive. /// /// This span. /// Other string. Can be null. /// /// Uses ordinal comparison (does not depend on current culture/locale). /// public static bool Eq(this RStr t, RStr s) => t.Equals(s, StringComparison.Ordinal); /// /// Returns true if equals to string s, case-insensitive. /// /// This span. /// Other string. Can be null. /// /// Uses ordinal comparison (does not depend on current culture/locale). /// public static bool Eqi(this RStr t, RStr s) => t.Equals(s, StringComparison.OrdinalIgnoreCase); /// /// Compares part of this span with string s. Returns true if equal. /// /// This span. /// Offset in this span. If invalid, returns false. /// Other string. /// Case-insensitive. /// s is null. /// /// Uses ordinal comparison (does not depend on current culture/locale). /// public static bool Eq(this RStr t, int startIndex, RStr s, bool ignoreCase = false) { int ns = s.LengthThrowIfNull_(); int to = startIndex + ns, tlen = t.Length; if (to > tlen || (uint)startIndex > tlen) return false; t = t[startIndex..to]; if (!ignoreCase) return t.SequenceEqual(s); return t.Equals(s, StringComparison.OrdinalIgnoreCase); } /// /// Returns true if the specified character is at the specified position in this span. /// /// This span. /// Offset in this span. If invalid, returns false. /// Character. public static bool Eq(this RStr t, int index, char c) { if ((uint)index >= t.Length) return false; return t[index] == c; } /// /// Returns true if starts with string s. /// /// This span. /// Other string. /// Case-insensitive. /// /// Uses ordinal comparison (does not depend on current culture/locale). /// public static bool Starts(this RStr t, RStr s, bool ignoreCase = false) => t.StartsWith(s, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); /// /// Returns true if starts with string s. /// /// This span. /// Other string. /// Case-insensitive. /// /// Uses ordinal comparison (does not depend on current culture/locale). /// public static bool Ends(this RStr t, RStr s, bool ignoreCase = false) => t.EndsWith(s, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); /// /// Finds character c in this span, starting from startIndex. /// /// Character index in this span, or -1 if not found. /// public static int IndexOf(this RStr t, int startIndex, char c) { int i = t[startIndex..].IndexOf(c); return i < 0 ? i : i + startIndex; } /// /// Finds character c in range of this span. /// /// Character index in this span, or -1 if not found. /// public static int IndexOf(this RStr t, Range range, char c) { int i = t[range].IndexOf(c); if (i < 0) return i; return i + range.Start.GetOffset(t.Length); } /// /// Finds string s in this span, starting from startIndex. /// /// Character index in this span, or -1 if not found. /// /// s is null. public static int IndexOf(this RStr t, int startIndex, RStr s, bool ignoreCase = false) { if (s.IsNull()) throw new ArgumentNullException(); int i = t[startIndex..].IndexOf(s, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); return i < 0 ? i : i + startIndex; } /// /// Finds string s in range of this span. /// /// Character index in this span, or -1 if not found. /// /// s is null. public static int IndexOf(this RStr t, Range range, RStr s, bool ignoreCase = false) { if (s.IsNull()) throw new ArgumentNullException(); int i = t[range].IndexOf(s, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); if (i < 0) return i; return i + range.Start.GetOffset(t.Length); } #if !DEBUG /// /// Alias of IndexOfAnyExcept. /// [EditorBrowsable(EditorBrowsableState.Never)] //.NET now has IndexOfAnyExcept public static int IndexOfNot(this RStr t, string chars) => t.IndexOfAnyExcept(chars); /// /// Alias of LastIndexOfAnyExcept. /// [EditorBrowsable(EditorBrowsableState.Never)] //.NET now has LastIndexOfAnyExcept public static int LastIndexOfNot(this RStr t, string chars) => t.LastIndexOfAnyExcept(chars); #endif internal static void CopyTo_(this string t, char* p) => t.AsSpan().CopyTo(new Span(p, t.Length)); /// /// Converts to UTF-8 (Encoding.UTF8.GetBytes). /// public static byte[] ToUTF8(this string t) => Encoding.UTF8.GetBytes(t); /// /// Converts to UTF-8. /// /// Return 0-terminated UTF-8 string. public static byte[] ToUTF8(this RStr t, bool append0 = false) => Convert2.Utf8Encode(t, append0 ? "\0" : null); /// /// Converts to UTF-8. /// /// Return 0-terminated UTF-8 string. public static byte[] ToUTF8(this Span t, bool append0 = false) => Convert2.Utf8Encode(t, append0 ? "\0" : null); /// /// Converts UTF-8 string to string. /// public static string ToStringUTF8(this byte[] t) => Encoding.UTF8.GetString(t); /// /// Converts UTF-8 string to string. /// public static string ToStringUTF8(this RByte t) => Encoding.UTF8.GetString(t); /// /// Converts UTF-8 string to string. /// public static string ToStringUTF8(this Span t) => Encoding.UTF8.GetString(t); /// /// Splits this string. Trims, and removes empty. /// /// Array containing 0 or more strings. internal static string[] Split_(this string t, char c) { return t.Split(c, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); } /// /// Splits this string and creates HashSet. Trims, and removes empty. /// /// HashSet containing 0 or more strings. internal static HashSet SplitHS_(this string t, char c, bool ignoreCase) { return t.Split_(c).ToHashSet(ignoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal); } /// /// Splits string s into 2 strings. /// Tip: there are 2 overloads: for string and for RStr. /// /// /// Separator character. /// String at the left. /// String at the right. /// Minimal s1 length. /// Minimal s2 length. /// Returns false if character c not found or if strings are too short. /// /// Can be used to split strings like "name=value" or "name: value" or "s1 s2" etc. Trims spaces. /// internal static bool Split2_(this string t, char c, out string s1, out string s2, int minLen1, int minLen2) { if (!Split2_(t, c, out RStr k1, out RStr k2, minLen1, minLen2)) { s1 = s2 = null; return false; } s1 = k1.ToString(); s2 = k2.ToString(); return true; } /// internal static bool Split2_(this RStr t, char c, out RStr s1, out RStr s2, int minLen1, int minLen2) { t = t.Trim(); int i = t.IndexOf(c); if (i >= 0) { s1 = t[..i].TrimEnd(); s2 = t[++i..].TrimStart(); if (s1.Length >= minLen1 && s2.Length >= minLen2) return true; } s1 = default; s2 = default; return false; } /// /// If index is in this string, returns character at index. Else '\0'. /// internal static char At_(this string t, int index) => (uint)index < t.Length ? t[index] : default; /// /// If index is in this string span, returns character at index. Else '\0'. /// internal static char At_(this RStr t, int index) => (uint)index < t.Length ? t[index] : default; } /// /// Flags for and similar functions. /// [Flags] public enum STIFlags { /// /// Don't support hexadecimal numbers (numbers with prefix "0x"). /// NoHex = 1, /// /// The number in string is hexadecimal without a prefix, like "1A". /// IsHexWithout0x = 2, /// /// Fail if string starts with a whitespace character. /// DontSkipSpaces = 4, } /// /// Used with /// public enum SUpper { /// /// Convert all characters to upper case. /// AllChars, /// /// Convert only the first character to upper case. /// FirstChar, /// /// Convert the first character of each word to upper case and other characters to lower case. /// Calls . /// TitleCase, } ================================================ FILE: Au/String/SegParser.cs ================================================ //Modified version of Microsoft.Extensions.Primitives.StringSegment. It is from github; current .NET does not have it, need to get from NuGet. //Can be used instead of String.Split, especially when you want less garbage. Faster (the github version with StringTokenizer was slower). #if !DEBUG namespace Au.More { /// /// Splits a string into substrings as start/end offsets or strings. /// /// /// Can be used with foreach. Normally you don't create SegParser instances explicitly; instead use with foreach. /// [EditorBrowsable(EditorBrowsableState.Never)] //obsolete. See comments in ExtString.Segments. public struct SegParser : IEnumerable, IEnumerator { readonly string _separators; readonly string _s; readonly int _sStart, _sEnd; int _start, _end; ushort _sepLength; SegFlags _flags; /// /// Initializes this instance to split a string. /// /// The string. /// A string containing characters that delimit substrings. Or one of constants. /// /// Part of the string to split. public SegParser(string s, string separators, SegFlags flags = 0, Range? range = null) { _separators = separators; _s = s; _sepLength = 1; _flags = flags; (_sStart, _sEnd) = range.GetStartEnd(s.Length); _start = 0; _end = _sStart - 1; } #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member public SegParser GetEnumerator() => this; IEnumerator IEnumerable.GetEnumerator() => this; IEnumerator IEnumerable.GetEnumerator() => this; public StartEnd Current => new(_start, _end); object IEnumerator.Current => Current; [MethodImpl(MethodImplOptions.AggressiveOptimization)] public bool MoveNext() { gStart: int i = _end + _sepLength, to = _sEnd; if (i > to) return false; _start = i; string s = _s, sep = _separators; switch (sep.Length) { case 1: { var c = sep[0]; for (; i < to; i++) if (s[i] == c) goto g1; } break; case 22: if (ReferenceEquals(sep, SegSep.Whitespace)) { for (; i < to; i++) if (char.IsWhiteSpace(s[i])) goto g1; } else if (ReferenceEquals(sep, SegSep.Word)) { for (; i < to; i++) if (!char.IsLetterOrDigit(s[i])) goto g1; } else if (ReferenceEquals(sep, SegSep.Line)) { _sepLength = 1; for (; i < to; i++) { var c = s[i]; if (c > '\r') continue; if (c == '\r') goto g2; else if (c == '\n') goto g1; } break; g2: if (i < to - 1 && s[i + 1] == '\n') _sepLength = 2; break; } else goto default; break; default: { for (; i < to; i++) { var c = s[i]; for (int j = 0; j < sep.Length; j++) if (c == sep[j]) goto g1; //speed: reverse slower } } break; } g1: _end = i; if (i == _start && 0 != (_flags & SegFlags.NoEmpty)) goto gStart; return true; } void IDisposable.Dispose() { //rejected. Normally this variable is not reused because GetEnumerator returns a copy. //_end = _sStart - 1; } public void Reset() { _end = _sStart - 1; } #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member /// /// Returns segment values as string[]. /// /// The maximal number of substrings to get. If negative (default), gets all. Else if there are more substrings, the last element will contain single substring, unlike with . [MethodImpl(MethodImplOptions.AggressiveOptimization)] public unsafe string[] ToStringArray(int maxCount = -1) { //All this big code is just to make this function as fast as String.Split. Also less garbage. // Simple code is slower when substrings are very short. // With short substrings, the parsing code takes less than half of time. Creating strings and arrays is the slow part. string100 a1 = new(); //at first use array on stack. When it is filled, use a2. Note: no [SkipLocalsInit], because references are always inited. List a2 = null; int n = 0; for (; MoveNext(); n++) { //slightly faster than foreach if (maxCount >= 0 && n == maxCount) break; var s = _s[_start.._end]; if (n < c_a1Size) { a1[n] = s; } else { a2 ??= new List(c_a1Size * 2); a2.Add(s); } } _end = _sStart - 1; //reset, because this variable can be reused later. never mind: try/finally; it makes slightly slower. var r = new string[n]; for (int i = 0, to = Math.Min(n, c_a1Size); i < to; i++) { r[i] = a1[i]; a1[i] = null; } for (int i = c_a1Size; i < r.Length; i++) { r[i] = a2[i - c_a1Size]; } return r; } const int c_a1Size = 50; [InlineArray(c_a1Size)] struct string100 { string s; } } } namespace Au.Types { /// /// Contains several string constants that can be used with some "split string" functions of this library to specify separators. /// [EditorBrowsable(EditorBrowsableState.Never)] //obsolete public static class SegSep { /// /// Specifies that separators are spaces, tabs, newlines and other characters for which returns true. /// public const string Whitespace = "SSlkGrJUMUutrbSK3s6Crw"; /// /// Specifies that separators are all characters for which returns false. /// public const string Word = "WWVL0EtrK0ShqYWb4n1CmA"; /// /// Specifies that separators are substrings "\r\n", as well as single characters '\r' and '\n'. /// public const string Line = "LLeg5AWCNkGTZDkWuyEa2g"; //note: all must be of length 22. } /// /// Flags for and some other functions. /// [Flags] [EditorBrowsable(EditorBrowsableState.Never)] //obsolete public enum SegFlags : byte { /// /// Don't return empty substrings. /// For example, is string is "one two " and separators is " ", return {"one", "two"} instead of {"one", "", "two", ""}. /// NoEmpty = 1, } } #endif ================================================ FILE: Au/String/StringUtil.cs ================================================ using System.Text.Json; namespace Au.More; /// /// Miscellaneous rarely used string functions. Parsing etc. /// public static class StringUtil { /// /// Parses a function parameter that can optionally have a "***name " prefix, like "***value xyz". /// /// 0 - s does not start with "***"; i+1 - s starts with "***names[i] "; -1 - s is invalid. /// Parameter. If starts with "***" and is valid, receives the "value" part; else unchanged. Can be null. /// List of supported "name". /// /// Used to parse parameters like name of . /// internal static int ParseParam3Stars_(ref string s, params ReadOnlySpan names) { if (s == null || !s.Starts("***")) return 0; for (int i = 0; i < names.Length; i++) { var ni = names[i]; if (s.Length - 3 <= ni.Length || !s.Eq(3, ni)) continue; int j = 3 + ni.Length; char c = s[j]; if (c != ' ') break; s = s[(j + 1)..]; return i + 1; } return -1; } /// /// Removes characters used to underline next character when the text is displayed in UI. Replaces two such characters with single. /// /// Can be null. /// /// /// Character '&' (in WPF '_') is used to underline next character in displayed text of dialog controls and menu items. Two such characters are used to display single. /// The underline is displayed when using the keyboard with Alt key to select dialog controls and menu items. /// [SkipLocalsInit] public static unsafe string RemoveUnderlineChar(string s, char underlineChar = '&') { if (s != null && s.Contains(underlineChar)) { using FastBuffer b = new(s.Length); int j = 0; bool was = false; for (int i = 0; i < s.Length; i++) { if (s[i] == underlineChar) { if (i < s.Length - 1 && s[i + 1] == underlineChar) i++; else if (!was) { was = underlineChar == '_'; continue; } //WPF removes only first single _ } b.p[j++] = s[i]; } s = new string(b.p, 0, j); } return s; } /// /// Finds character used to underline next character when the text is displayed in UI. /// /// Character index, or -1 if not found. /// Can be null. /// public static int FindUnderlineChar(string s, char underlineChar = '&') { if (s != null) { for (int i = 0; i < s.Length; i++) { if (s[i] == underlineChar) { if (++i < s.Length && s[i] != underlineChar) return i; } } } return -1; } /// /// Converts array of command line arguments to string that can be passed to a "start process" function, for example , . /// /// null if a is null or empty. /// public static string CommandLineFromArray(string[] a) { if (a == null || a.Length == 0) return null; StringBuilder b = null; foreach (var v in a) { int esc = 0; if (v.NE()) esc = 1; else if (v.Contains('"')) esc = 2; else foreach (var c in v) if (c <= ' ') { esc = 1; break; } if (esc == 0 && a.Length == 1) return a[0]; if (b == null) b = new StringBuilder(); else b.Append(' '); if (esc == 0) b.Append(v); else { b.Append('"'); var s = v; if (esc == 2) { if (s.Find(@"\""") < 0) s = s.Replace(@"""", @"\"""); else s = s.RxReplace(@"(\\*)""", @"$1$1\"""); } if (s.Ends('\\')) s = s.RxReplace(@"(\\+)$", "$1$1"); b.Append(s).Append('"'); } } return b.ToString(); } /// /// Parses command line arguments. /// Calls API CommandLineToArgvW. /// /// Empty array if s is null or "". public static unsafe string[] CommandLineToArray(string s) { if (s.NE()) return []; char** p = Api.CommandLineToArgvW(s, out int n); var a = new string[n]; for (int i = 0; i < n; i++) a[i] = new string(p[i]); Api.LocalFree(p); return a; } /// /// If string contains a number at startIndex, gets that number as int, also gets the string part that follows it, and returns true. /// /// /// Receives the number. Receives 0 if no number. /// Receives the string part that follows the number, or "". Receives null if no number. Can be this variable. /// Offset in this string where to start parsing. /// /// /// For example, for string "25text" or "25 text" gets num = 25, tail = "text". /// Everything else is the same as with . /// public static bool ParseIntAndString(string s, out int num, out string tail, int startIndex = 0, STIFlags flags = 0) { num = s.ToInt(startIndex, out int end, flags); if (end == 0) { tail = null; return false; } if (end < s.Length && s[end] == ' ') end++; tail = s[end..]; return true; } /// /// Creates int[] from string containing space-separated numbers, like "4 100 -8 0x10". /// /// Decimal or/and hexadecimal numbers separated by single space. If null or "", returns empty array. /// /// For vice versa use string.Join(" ", array). /// public static int[] StringToIntArray(string s) { if (s.NE()) return []; int n = 1; foreach (var v in s) if (v == ' ') n++; var a = new int[n]; a[0] = s.ToInt(0, STIFlags.DontSkipSpaces); for (int i = 0, j = 0; j < s.Length;) if (s[j++] == ' ') a[++i] = s.ToInt(j, STIFlags.DontSkipSpaces); return a; } /// /// Converts character index in string to line index and character index in that line. /// /// /// Character index in string s. /// Receives 0-based line index. /// Receives 0-based character index in that line. /// public static void LineAndColumn(string s, int index, out int lineIndex, out int indexInLine) { if ((uint)index > s.Length) throw new ArgumentOutOfRangeException(); int line = 0, lineStart = 0; for (int i = 0; i < index; i++) { char c = s[i]; if (c > '\r') continue; if (c != '\n') { if (c != '\r') continue; if (i < s.Length - 1 && s[i + 1] == '\n') continue; } lineStart = i + 1; line++; } lineIndex = line; indexInLine = index - lineStart; } /// /// Converts flags to string where flags are mapped to characters. /// /// /// Don't use ASCII digits for flags, because interprets a number-string as the flags value. Any other characters are OK, not only letters. /// public static string FlagsToLetters(int flags, params (int flag, char c)[] map) { Span a = stackalloc char[32]; int len = 0; foreach (var (f, c) in map) { if (0 != (flags & f)) a[len++] = c; } return a[..len].ToString(); } /// /// Parses flags from string where flags are mapped to characters. See /// /// /// If the string is a number, interprets it as the flags value. Don't use ASCII digits for flags. Any other characters are OK, not only letters. /// public static int FlagsFromLetters(string s, params (int flag, char c)[] map) { int r = 0; if (!s.NE() && !s.ToInt(out r)) { foreach (var (f, c) in map) { if (s.Contains(c)) r |= f; } } return r; } /// /// Calculates the Levenshtein distance between two strings, which tells how much they are different. /// /// /// It is the number of character edits (removals, inserts, replacements) that must occur to get from string s1 to string s2. /// Can be used to measure similarity and match approximate strings with fuzzy logic. /// Uses code and info from . /// public static int LevenshteinDistance(RStr s1, RStr s2) { int n = s1.Length; int m = s2.Length; // Step 1 if (n == 0) return m; if (m == 0) return n; // Step 2 int[,] d = new int[n + 1, m + 1]; for (int i = 0; i <= n; d[i, 0] = i++) { } for (int j = 0; j <= m; d[0, j] = j++) { } // Step 3 for (int i = 1; i <= n; i++) { //Step 4 for (int j = 1; j <= m; j++) { // Step 5 int cost = (s2[j - 1] == s1[i - 1]) ? 0 : 1; // Step 6 d[i, j] = Math.Min( Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost); } } // Step 7 return d[n, m]; } /// /// Returns the number of characters common to the start of each string. /// public static int CommonPrefix(RStr s1, RStr s2) { int n = Math.Min(s1.Length, s2.Length); for (int i = 0; i < n; i++) { if (s1[i] != s2[i]) return i; } return n; } /// /// Returns the number of characters common to the end of each string. /// public static int CommonSuffix(RStr s1, RStr s2) { int len1 = s1.Length; int len2 = s2.Length; int n = Math.Min(len1, len2); for (int i = 1; i <= n; i++) { if (s1[len1 - i] != s2[len2 - i]) return i - 1; } return n; } /// /// Converts JSON element to multiline indented JSON string. /// public static string JsonMultiline(JsonElement json) { var so = s_jsOptions.Value; return JsonSerializer.Serialize(json, so); } /// /// Converts single-line JSON string to multiline indented JSON string. /// public static string JsonMultiline(string json) { var so = s_jsOptions.Value; var v = JsonSerializer.Deserialize(json, so); return JsonSerializer.Serialize(v, so); } static readonly Lazy s_jsOptions = new(() => new() { WriteIndented = true //Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, }); internal static JsonSerializerOptions JsonOptions_ => s_jsOptions.Value; /// Calls , and if need. /// null if failed. public static Encoding GetEncoding(string name) => _GetEncoding(name ?? throw new ArgumentNullException()); /// Calls , and if need. /// If -1, uses the current Windows ANSI code page (API GetACP). /// null if failed. public static Encoding GetEncoding(int codepage) => _GetEncoding(null, codepage == -1 ? Api.GetACP() : codepage); static Encoding _GetEncoding(string name = null, int codepage = 0) { g1: try { return name != null ? Encoding.GetEncoding(name) : Encoding.GetEncoding(codepage); } catch { if (Interlocked.CompareExchange(ref s_encodingInited, 1, 0) == 0) { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); goto g1; } } return null; } static int s_encodingInited; } ================================================ FILE: Au/String/csvTable.cs ================================================ namespace Au; /// /// Parses and composes CSV text. CSV table data in memory as a List of string arrays. /// /// /// CSV is a text format used to store a single table of data in human-readable/editable way. /// It is a list of lines (called rows or records) containing one or more values (called fields or cells) separated by a separator character. /// /// There is no strictly defined CSV standard. This class uses these rules: ///
• Fields containing separator characters (default ','), quote characters (default '"') and multiple lines are enclosed in quote characters. Example: "ab, cd". ///
• Each quote character in such fields is escaped (replaced) with two quote characters. Example: "ab ""cd"" ef". ///
• If a field value starts or ends with ASCII space or tab characters, it is enclosed in quote characters. Example: " ab ". Or use parameter trimSpaces false when parsing. ///
• Rows in CSV text can have different field count. All rows in in-memory CSV table have equal field count. ///
/// /// /// public class csvTable { readonly List _a; /// /// Initializes new variable that can be used to add rows. /// To create new variables from CSV text, file or dictionary, instead use static functions, for example . /// public csvTable() { _a = new(); } csvTable(List a, int columnCount) { _a = a; _columnCount = columnCount; } /// /// Gets the internal List containing rows as string arrays. /// /// /// It's not a copy; changing its content will change content of this variable. /// You can do anything with the List. For example, sort it, find rows containing certain field values, get/set field values directly, add/remove rows directly. /// All row arrays have Length equal to , and it must remain so; you can change Length, but then need to call ColumnCount=newLength. /// /// /// string.CompareOrdinal(a[0], b[0])); /// ]]> /// public List Rows => _a; /// /// Sets or gets the field separator character used when composing CSV text. /// Initially it is ','. /// public char Separator { get; set; } = ','; /// /// Sets or gets the quote character used when composing CSV text. /// Initially it is '"'. /// public char Quote { get; set; } = '"'; /// /// Parses CSV string and creates new variable that contains data in internal List of string arrays. /// /// /// CSV text. /// If rows in CSV text have different field count, the longest row sets the property and lengths of all row arrays; array elements of missing CSV fields will be null. /// /// Field separator character used in CSV text. Default ','. /// Character used in CSV text to enclose some fields. Default '"'. /// Ignore ASCII space and tab characters surrounding fields in CSV text. Default true. /// Invalid CSV, eg contains incorrectly enclosed fields. [MethodImpl(MethodImplOptions.AggressiveOptimization)] public static unsafe csvTable parse(string csv, char separator = ',', char quote = '"', bool trimSpaces = true) { if (csv.NE()) return new csvTable(); var a = new List(); var tempRow = new List(8); string sQuote1 = null, sQuote2 = null; fixed (char* s0 = csv) { char* s = s0, se = s0 + csv.Length; int nCol = 0; for (; s < se; s++) { //Read a field. string field = ""; if (trimSpaces) { //ltrim while (*s == ' ' || *s == '\t') if (++s == se) goto g1; } char* f, e; //field beginning and end, not including spaces bool hasEscapedQuote = false; if (*s == quote) { f = ++s; bool hasClosingQuote = false; while (s < se) { if (*s++ == quote) { if (s < se && *s == quote) { s++; hasEscapedQuote = true; } //escaped quote else { hasClosingQuote = true; break; } } } if (!hasClosingQuote) throw new FormatException($"Invalid CSV format. Cells that start with {quote} must end with {quote}."); e = s - 1; //before quote if (trimSpaces) { //rtrim while (s < se && (*s == ' ' || *s == '\t')) s++; } if (s < se && !(*s == separator || *s == '\n')) { if (*s == '\r' && s < se + 1 && s[1] == '\n') s++; else throw new FormatException($"Invalid CSV format. For {quote} in enclosed cells use {quote}{quote}."); } } else { f = s; //field start while (s < se && *s != separator && *s != '\n') s++; //skip field, space and \r e = s; if (e > f && *e == '\n' && e[-1] == '\r') e--; if (trimSpaces) { //rtrim while (e > f && (e[-1] == ' ' || e[-1] == '\t')) e--; } } field = new string(f, 0, (int)(e - f)); //field to string if (hasEscapedQuote) { if (sQuote1 == null) { sQuote1 = new string(quote, 1); sQuote2 = new string(quote, 2); } field = field.Replace(sQuote2, sQuote1); } g1: //print.it(field); tempRow.Add(field); if (s >= se || *s == '\n') { //print.it(a.Count); //print.it(tempRow); a.Add(tempRow.ToArray()); if (tempRow.Count > nCol) nCol = tempRow.Count; tempRow.Clear(); } } var R = new csvTable(a, 0); R.ColumnCount = nCol; //make all rows of equal length and set _columnCount return R; } //fixed } /// /// Composes CSV text from the internal List of string arrays. /// /// /// Depends on these properties: (initially ','), (initially '"'). /// public override string ToString() { if (RowCount == 0 || ColumnCount == 0) return ""; using (new StringBuilder_(out var b)) { char quote = Quote; string sQuote1 = null, sQuote2 = null; for (int r = 0; r < _a.Count; r++) { for (int c = 0; c < _columnCount; c++) { var field = _a[r][c]; if (!field.NE()) { bool hasQuote = field.Contains(quote); if (hasQuote || field.Contains(Separator) || field[0] == ' ' || field[^1] == ' ') { if (hasQuote) { if (sQuote1 == null) { sQuote1 = new string(quote, 1); sQuote2 = new string(quote, 2); } field = field.Replace(sQuote1, sQuote2); } b.Append(quote).Append(field).Append(quote); } else b.Append(field); } if (c < _columnCount - 1) b.Append(Separator); } b.AppendLine(); } return b.ToString(); } } /// /// Gets or sets row count. /// The get function returns the Count property of the internal List of string arrays. /// The set function can add new rows or remove rows at the end. /// public int RowCount { get => _a.Count; set { if (value > _a.Count) { _a.Capacity = value; while (_a.Count < value) _a.Add(_columnCount > 0 ? new string[_columnCount] : null); } else if (value < _a.Count) { _a.RemoveRange(value, _a.Count - value); } } } /// /// Gets or sets column count. /// The get function returns the length of all string arrays in the internal List. /// The set function can add new columns or remove columns at the right. /// public int ColumnCount { get => _columnCount; set { if (value <= 0) throw new ArgumentOutOfRangeException(); //if(value == _columnCount) return; for (int r = 0; r < _a.Count; r++) { var o = _a[r]; if (o == null) _a[r] = new string[value]; else if (o.Length != value) { var t = new string[value]; if (value > _columnCount) o.CopyTo(t, 0); else { for (int c = 0; c < value; c++) t[c] = o[c]; } _a[r] = t; } } _columnCount = value; } } int _columnCount; /// /// Gets or sets a field. /// /// 0-based row index. The set function adds new row if negative or equal to . /// 0-based column index. With the set function it can be >= and < 1000; then sets ColumnCount = column + 1. /// /// /// /// public string this[int row, int column] { get { if ((uint)column >= _columnCount) throw new ArgumentOutOfRangeException("column"); if ((uint)row >= RowCount) throw new ArgumentOutOfRangeException("row"); return _a[row][column]; } set { //Auto-add columns. if (column < 0) column = int.MaxValue; if (column >= _columnCount) { if (column >= 1000) throw new ArgumentOutOfRangeException("column"); ColumnCount = column + 1; } //Auto-add row. if (row < 0) row = RowCount; if (row >= RowCount) { if (row > RowCount) throw new ArgumentOutOfRangeException("row"); _a.Add(new string[_columnCount]); } _a[row][column] = value; } } /// /// Gets or sets a field. /// /// 0-based row index. Can be from the end; for example ^1 is the last row. The set function adds new row if ^0. /// 0-based column index. With the set function it can be >= and < 1000; then sets ColumnCount = column + 1. /// public string this[Index row, int column] { get => this[row.GetOffset(_a.Count), column]; set { this[row.GetOffset(_a.Count), column] = value; } } /// /// Gets or sets fields in a row. /// /// 0-based row index. The set function adds new row if negative or equal to . /// /// /// The get function gets the row array. It's not a copy; changing its elements will change content of this variable. /// The set function sets the row array. Does not copy the array, unless its Length is less than . /// /// /// /// public string[] this[int row] { get { if ((uint)row >= RowCount) throw new ArgumentOutOfRangeException(); return _a[row]; } set { //Auto-add row. if (row < 0) row = RowCount; if (row >= RowCount) { if (row > RowCount) throw new ArgumentOutOfRangeException(); _a.Add(null); } var t = value; if (value == null || value.Length < _columnCount) { //make row length = _columnCount t = new string[_columnCount]; value?.CopyTo(t, 0); } else if (value.Length > _columnCount) { //auto-add columns ColumnCount = value.Length; } _a[row] = t; } } /// /// Gets or sets fields in a row. /// /// 0-based row index. Can be from the end; for example ^1 is the last row. The set function adds new row if ^0. /// /// /// The get function gets the row array. It's not a copy; changing its elements will change content of this variable. /// The set function sets the row array. Does not copy the array, unless its Length is less than . /// public string[] this[Index row] { get => this[row.GetOffset(_a.Count)]; set { this[row.GetOffset(_a.Count)] = value; } } /// /// Adds new row and sets its fields. /// /// Row fields. Can be a string array or multiple string arguments. Does not copy the array, unless its Length is less than . Adds new columns if array Length (or the number of string arguments) is greater than ColumnCount. /// public void AddRow(params string[] fields) => InsertRow(-1, fields); /// /// Inserts new row and sets its fields. /// /// 0-based row index. If negative or equal to , adds to the end. /// Row fields. Can be a string array or multiple string arguments. Does not copy the array, unless its Length is less than . Adds new columns if array Length (or the number of string arguments) is greater than ColumnCount. /// public void InsertRow(int index, params string[] fields) { if (index < 0) index = RowCount; _a.Insert(index, null); this[index] = fields; } /// /// Inserts new empty row. /// /// 0-based row index. If negative or equal to , adds to the end. /// public void InsertRow(int index) { InsertRow(index, null); } /// /// Removes one or more rows. /// /// 0-based row index. /// How many rows to remove, default 1. /// /// public void RemoveRow(int index, int count = 1) { _a.RemoveRange(index, count); } //FUTURE: implement these. Rarely used. Quite much code. //Don't need to implement many others because users can call our _a (the Data property) methods directly. //public void MoveRow(int from, int to) //{ //} //public void InsertColumn(int index) //{ //} //public void RemoveColumn(int index) //{ //} /// /// Loads and parses a CSV file. /// /// File. Must be full path. Can contain environment variables etc, see . /// Field separator character used in CSV text. Default ','. /// Character used in CSV text to enclose some fields. Default '"'. /// Ignore ASCII space and tab characters surrounding fields in CSV text. Default true. /// Not full path. /// Exceptions of . /// Invalid CSV, eg contains incorrectly enclosed fields. /// /// Calls and . Also uses . /// public static csvTable load(string file, char separator = ',', char quote = '"', bool trimSpaces = true) { var csv = filesystem.loadText(file); return parse(csv, separator, quote, trimSpaces); } /// /// Composes CSV and saves to a file. /// /// File. Must be full path. Can contain environment variables etc, see . The file can exist or not; this function overwrites it. /// Create backup file named file + "~backup". /// Not full path. /// Exceptions of . /// /// Calls and . Also uses . /// public void Save(string file, bool backup = false) { var csv = ToString(); filesystem.saveText(file, csv, backup); } /// /// Creates 2-column CSV table from dictionary keys and values of type string. /// /// /// public static csvTable fromDictionary(Dictionary d) { Not_.Null(d); var a = new List(d.Count); foreach (var v in d) a.Add(new string[] { v.Key, v.Value }); return new csvTable(a, 2); } /// /// Creates 2-column CSV table from dictionary keys and values of any type, using a callback function to convert values to string. /// /// /// Callback function that converts value of type T to string. /// public static csvTable fromDictionary(Dictionary d, Func valueToString) { Not_.Null(d, valueToString); var a = new List(d.Count); foreach (var v in d) { var t = valueToString(v.Value); a.Add(new string[] { v.Key, t }); } return new csvTable(a, 2); } /// /// Creates CSV table of any column count from dictionary keys and values of any type, using a callback function to convert values to cell strings. /// /// /// CSV column count. Must be 2 or more. /// Callback function that converts value of type T to one or more strings and puts them in row array elements starting from index 1. At index 0 is key. /// /// columnCount less than 2. public static csvTable fromDictionary(Dictionary d, int columnCount, Action valueToCells) { Not_.Null(d, valueToCells); if (columnCount < 2) throw new ArgumentOutOfRangeException(); var a = new List(d.Count); foreach (var v in d) { var t = new string[columnCount]; t[0] = v.Key; valueToCells(v.Value, t); a.Add(t); } return new csvTable(a, columnCount); } /// /// Creates dictionary from this 2-column CSV table. /// /// Case-insensitive dictionary keys. /// Don't throw exception if column 0 contains duplicate strings. Replace old value with new value. /// not 2. /// Column 0 contains duplicate strings. public Dictionary ToDictionary(bool ignoreCase, bool ignoreDuplicates) { if (_columnCount != 2) throw new InvalidOperationException("ColumnCount must be 2"); var d = new Dictionary(ignoreCase ? StringComparer.OrdinalIgnoreCase : null); foreach (var v in _a) { if (ignoreDuplicates) d[v[0]] = v[1]; else d.Add(v[0], v[1]); } return d; } /// /// Creates dictionary from this CSV table of any column count, using a callback function to convert cell strings to dictionary values of any type. /// /// Case-insensitive dictionary keys. /// Don't throw exception if column 0 contains duplicate strings. Replace old value with new value. /// Callback function that converts one or more cell strings to single value of type T. The array is whole row; element 0 is key, and usually is not used. /// /// less than 2. /// Column 0 contains duplicate strings. public Dictionary ToDictionary(bool ignoreCase, bool ignoreDuplicates, Func rowToValue) { Not_.Null(rowToValue); if (_columnCount < 2) throw new InvalidOperationException("ColumnCount must be >= 2"); var d = new Dictionary(ignoreCase ? StringComparer.OrdinalIgnoreCase : null); foreach (var v in _a) { var t = rowToValue(v); if (ignoreDuplicates) d[v[0]] = t; else d.Add(v[0], t); } return d; } //rejected, because: 1. In some cases can fail to resolve overloads. 2. Almost duplicate of the string[] overload. ///// ///// Creates dictionary from this 2-column CSV table, using a callback function to convert cell strings to dictionary values of any type. ///// ///// Case-insensitive dictionary keys. ///// Callback function that converts cell string to value of type T. ///// ///// not 2. ///// Column 0 contains duplicate values. //public Dictionary ToDictionary(bool ignoreCase, Func stringToValue) //{ // if(stringToValue == null) throw new ArgumentNullException(); // if(_columnCount != 2) throw new InvalidOperationException("ColumnCount must be 2"); // var d = new Dictionary(ignoreCase ? StringComparer.OrdinalIgnoreCase : null); // foreach(var v in _a) d.Add(v[0], stringToValue(v[1])); // return d; //} /// /// Obsolete, use . /// [EditorBrowsable(EditorBrowsableState.Never)] public void SetInt(int row, int column, int value, bool hex = false) { this[row, column] = hex ? "0x" + value.ToString("X") : value.ToString(); } /// /// Obsolete, use . /// [EditorBrowsable(EditorBrowsableState.Never)] public int GetInt(int row, int column) { return this[row, column].ToInt(); } /// /// Obsolete, use . /// [EditorBrowsable(EditorBrowsableState.Never)] public void SetDouble(int row, int column, double value) { this[row, column] = value.ToS(); } /// /// Obsolete, use . /// [EditorBrowsable(EditorBrowsableState.Never)] public double GetDouble(int row, int column) { this[row, column].ToNumber(out double R); return R; } /// /// Converts a number to string and sets a field. /// /// 0-based row index. Can be from the end; for example ^1 is the last row. Adds new row if ^0. /// 0-based column index. If >= and < 1000, sets ColumnCount = column + 1. /// /// Invalid row or column. public void Set(Index row, int column, int value) { this[row, column] = value.ToS(); } /// /// Converts a number to hex string and sets a field. /// /// public void Set(Index row, int column, uint value) { this[row, column] = "0x" + value.ToString("X"); } /// public void Set(Index row, int column, long value) { this[row, column] = value.ToS(); } /// public void Set(Index row, int column, ulong value) { this[row, column] = "0x" + value.ToString("X"); } /// public void Set(Index row, int column, double value) { this[row, column] = value.ToS(); } /// public void Set(Index row, int column, float value) { this[row, column] = value.ToS(); } /// /// Converts a bool to string "true" or "false" and sets a field. /// /// public void Set(Index row, int column, bool value) { this[row, column] = value ? "true" : "false"; } /// /// Gets a field value converted to int. See . /// /// 0-based row index. /// 0-based column index. /// Receives the result, or 0 if failed. /// False if failed to convert from string. /// Invalid row or column. public bool Get(Index row, int column, out int value) => this[row, column].ToInt(out value); /// /// Gets a field value converted to uint. See . /// /// public bool Get(Index row, int column, out uint value) => this[row, column].ToInt(out value); /// /// Gets a field value converted to long. See . /// /// public bool Get(Index row, int column, out long value) => this[row, column].ToInt(out value); /// /// Gets a field value converted to ulong. See . /// /// public bool Get(Index row, int column, out ulong value) => this[row, column].ToInt(out value); /// /// Gets a field value converted to double. See . /// /// public bool Get(Index row, int column, out double value) => this[row, column].ToNumber(out value); /// /// Gets a field value converted to float. See . /// /// public bool Get(Index row, int column, out float value) => this[row, column].ToNumber(out value); /// /// Gets a field value like "true" or "false" converted to bool. Case-insensitive. /// /// public bool Get(Index row, int column, out bool value) => Boolean.TryParse(this[row, column], out value); //rejected: ToXml, ToHtml. Could be pasted in Excel, but need special format, difficult to make fully compatible. OpenOffice supports only HTML. } ================================================ FILE: Au/String/regexp.cs ================================================ using System.Text.RegularExpressions; //for XML doc links namespace Au; /// /// PCRE regular expression. /// /// /// PCRE is a regular expression library: . /// PCRE regular expression syntax: full, short. /// Some websites with tutorials and info: rexegg, regular-expressions.info. /// /// This class is an alternative to the .NET class. The regular expression syntax is similar. PCRE has some features unavailable in .NET, and vice versa. In most cases PCRE is faster. You can use any of these classes. Functions of class support only PCRE. /// /// Terms used in this documentation and in names of functions and types: /// - regular expression - regular expression string. Also known as pattern. /// - subject string - the string in which to search for the regular expression. Also known as input string. /// - match - the part (substring) of the subject string that matches the regular expression. /// - groups - regular expression parts enclosed in (). Except non-capturing parts, like (?:...) and (?options). Also known as capturing group, capturing subpattern. Often term group also is used for group matches. /// - group match - the part (substring) of the subject string that matches the group. Also known as captured substring. /// /// This library uses an unmanaged code dll AuCpp.dll that contains PCRE code. This class is a managed wrapper for it. The main PCRE API functions used by this class are pcre2_compile and pcre2_match. The regexp constructor calls pcre2_compile and stores the compiled code in the variable. Other regexp functions call pcre2_match. Compiling to native code (JIT) is not supported. /// /// A regexp variable can be used by multiple threads simultaneously. /// /// Also there are several extension methods that use this class. The string variable is the subject string. These methods create and use cached regexp instances for speed. The regexp constructor does not use caching. /// /// /// o.Value.Upper())); /// print.it("//Replace with callback and ExpandReplacement:"); /// print.it(x.Replace(s, o => { if(o.Length > 5) return o.ExpandReplacement("'$2$1'"); else return o[1].Value; })); /// /// print.it("//Split:"); /// print.it(new regexp(@" *, *").Split(s)); /// ]]> /// Examples with extension methods. /// o.Value.Upper())); /// print.it("//RxReplace with callback and ExpandReplacement:"); /// print.it(s.RxReplace(rx, o => { if(o.Length > 5) return o.ExpandReplacement("'$2$1'"); else return o[1].Value; })); /// /// print.it("//RxReplace, get replacement count:"); /// if(0 != s.RxReplace(rx, "'$2$1'", out var s2)) print.it(s2); /// /// print.it("//RxReplace with callback, get replacement count:"); /// if(0 != s.RxReplace(rx, o => o.Value.Upper(), out var s3)) print.it(s3); /// /// print.it("//RxSplit:"); /// print.it(s.RxSplit(@" *, *")); /// ]]> public unsafe class regexp { readonly IntPtr _codeUnsafe; //pcre2_code_16*. Don't pass to PCRE API directly, because then GC can collect this object Cpp.PcreCalloutT _pcreCallout; //our callout that calls the user's callout. This field protects the delegates from GC. readonly byte _matchFlags; //RXMatchFlags specified in hi byte of ctor flags internal HandleRef _CodeHR => new HandleRef(this, _codeUnsafe); //pass this to PCRE API /// /// Compiles regular expression string. /// /// Regular expression. Cannot be null. /// /// Options. /// Default 0. Flag UTF is implicitly added if rx contains non-ASCII characters and not used flag NEVER_UTF. /// /// /// Invalid regular expression. Or failed to compile it for some other reason (unlikely). /// /// Calls PCRE API function pcre2_compile. /// /// PCRE regular expression syntax: full, short. /// /// Examples in class help: . /// public regexp([ParamString(PSFormat.Regexp)] string rx, RXFlags flags = 0) { Not_.Null(rx); _matchFlags = (byte)((ulong)flags >> 56); flags = (RXFlags)((ulong)flags & 0xffffff_ffffffff); _codeUnsafe = Cpp.Cpp_RegexCompile(rx, rx.Length, flags, out int codeSize, out BSTR errStr); if (_codeUnsafe == default) throw new ArgumentException(errStr.ToStringAndDispose()); GC.AddMemoryPressure(codeSize); } /// ~regexp() { //print.it("dtor"); if (_codeUnsafe == default) return; int codeSize = Cpp.Cpp_RegexDtor(_codeUnsafe); GC.RemoveMemoryPressure(codeSize); } /// /// Sets callout callback function. /// /// Callback delegate (eg lambda) or null. /// /// Callouts can be used to: ///
• Track the matching progress. ///
• Get all instances of a group that can match multiple times. ///
• Evaluate and reject some matches or match parts. ///
• Etc. /// The callback function is called by , , , , and similar functions, when they reach callout points in regular expression. To insert callout points use (?C), (?C1), (?C2), (?C'name') etc or pass flag AUTO_CALLOUT to the constructor. /// More info in PCRE help topic pcre2callout. /// See also: ///
/// /// Track the matching progress. /// link text"; /// var rx = @"(?C1)(?C3)[^<]*(?C4)"; /// var x = new regexp(rx); /// x.Callout = o => { print.it(o.callout_number, o.start_match, o.current_position, s[o.start_match..o.current_position], rx.Substring(o.pattern_position, o.next_item_length)); }; /// print.it(x.IsMatch(s)); /// ]]> /// Track the matching progress with flag AUTO_CALLOUT. /// print.it(o.current_position, o.pattern_position, rx.Substring(o.pattern_position, o.next_item_length)); /// print.it(x.IsMatch(s)); /// ]]> /// Get all instances of a group that can match multiple times. /// (); /// x.Callout = o => a.Add(o.LastGroupValue); /// if(!x.Match(s, out var m)) { print.it("no match"); return; } /// print.it(m[1]); /// print.it(a); //all numbers. m[2] contains only the last number. /// print.it(m[3]); /// ]]> /// Evaluate and reject some matches or match parts. This code rejects matches longer than 5. /// { int len = o.current_position - o.start_match; /*print.it(len);*/ if(len > 5) o.Result = 1; }; /// print.it(x.FindAll(s, 0)); /// ]]> /// public Action Callout { set { lock (this) { if (value == null) { _pcreCallout = null; } else { _pcreCallout = (void* calloutBlock, void* param) => { var b = new RXCalloutData(calloutBlock); value(b); return b.Result; }; } } } } /// /// Finds a named group and returns its 1-based index. Returns -1 if not found. /// /// /// Group name. /// In regular expression, to set name of group (text), use (?<NAME>text). /// /// /// Multiple groups have this name. /// /// public int GetGroupNumberOf(string groupName) { Not_.Null(groupName); fixed (char* p = groupName) { int R = Cpp.pcre2_substring_nametable_scan(_CodeHR, p, null, null); if (R <= 0) { if (R == -50) throw new ArgumentException("Multiple groups have name " + groupName); //-50 PCRE2_ERROR_NOUNIQUESUBSTRING R = -1; } return R; } } /// /// Returns the highest capture group number in the regular expression. If (?| not used, this is also the total count of capture groups. /// public int GetMaxGroupNumber() { int R = 0; Cpp.pcre2_pattern_info(_CodeHR, Cpp.PCRE2_INFO_.CAPTURECOUNT, &R); return R; } //Calls Cpp_RegexMatch and returns its results. //Throws if it returns less than -1. //m.vec array is thread_local. Next call reallocates/overwrites it, except when called by a callout of the same call. //m.mark is set even if no match, if available. //s - subject. If null, returns -1. //rawFlags - pass flags as is. If false, calls _GetMatchFlags. If true, flags must be result of _GetMatchFlags. //group - 0 or group number. Used only to throw if invalid. int _PcreMatch(RStr s, int start, RXMatchFlags flags, bool rawFlags, out Cpp.RegexMatch m, bool needM, int group = 0) { if (s.IsNull()) { m = default; return -1; } //null fixed (char* p = s) { if (!rawFlags) flags = _GetMatchFlags(flags); int rc = Cpp.Cpp_RegexMatch(_CodeHR, p != null ? p : (char*)&p, s.Length, start, flags, _pcreCallout, out m, needM, out BSTR errStr); if (rc < -1) throw new AuException(errStr.ToStringAndDispose()); if (group != 0 && rc >= 0 && (uint)group >= m.vecCount) throw new ArgumentOutOfRangeException(nameof(group)); return rc; //print.it(rc); //info: 0 is partial match, -1 is no match, <-1 is error } } //Gets span and returns start. //If range is null, sets span = s and returns 0. //Else if range is invalid, throws ArgumentOutOfRangeException. //Else sets span.Length = range and returns 0. In any case span.Start is 0. static int _GetSpan(string s, Range? range, out RStr span) { span = s; if (!range.HasValue) return 0; var (i, end) = range.GetStartEnd(span.Length); if (end != span.Length) span = span[..end]; return i; } static int _GetSpan(ref RStr span, Range? range) { if (!range.HasValue) return 0; var (i, end) = range.GetStartEnd(span.Length); if (end != span.Length) span = span[..end]; return i; } RXMatchFlags _GetMatchFlags(RXMatchFlags matchFlags, bool throwIfPartial = false) { var f = (RXMatchFlags)_matchFlags | matchFlags; if (throwIfPartial) { if (0 != (f & (RXMatchFlags.PARTIAL_SOFT | RXMatchFlags.PARTIAL_HARD))) throw new ArgumentException("This function does not support PARTIAL_ flags.", nameof(matchFlags)); } return f; } /// /// Returns true if string s matches this regular expression. /// /// true if full or partial match. Partial match is possible if used a PARTIAL_ flag. /// /// Subject string. /// If null, returns false, even if the regular expression matches empty string. /// /// /// Start and end offsets in the subject string. If null (default), uses whole string. /// Examples: i..j (from i to j), i.. (from i to the end), ..j (from 0 to j). /// The subject part before the start index is not ignored if regular expression starts with a lookbehind assertion or anchor, eg ^ or \b or (?<=...). Instead of ^ you can use \G or flag RXFlags.ANCHORED. More info in PCRE documentation topic pcre2api, chapter "The string to be matched by pcre2_match()". /// The subject part after the end index is always ignored. /// /// Options. /// The same options also can be set in constructor's flags. Constructor's flags and matchFlags are added, which means that matchFlags cannot unset flags set by constructor. /// /// Invalid range. /// The PCRE API function pcre2_match failed. Unlikely. /// /// This function is similar to . /// /// /// /// public bool IsMatch(string s, Range? range = null, RXMatchFlags matchFlags = 0) { int start = _GetSpan(s, range, out var span); return _PcreMatch(span, start, matchFlags, rawFlags: false, out _, needM: false) >= 0; } /// public bool IsMatch(RStr s, Range? range = null, RXMatchFlags matchFlags = 0) { int start = _GetSpan(ref s, range); return _PcreMatch(s, start, matchFlags, rawFlags: false, out _, needM: false) >= 0; } /// /// Returns true if string s matches this regular expression. /// Gets match info as . /// /// ///
• If full match, returns true, and result contains the match and all groups that exist in the regular expressions. ///
• If partial match, returns true, and result contains the match without groups. Partial match is possible if used a PARTIAL_ flag. ///
• If no match, returns false, and result normally is null. But if a mark is available, result is an object with two valid properties - (false) and ; other properties have undefined values or throw exception. ///
/// Receives match info. /// /// This function is similar to . /// /// /// /// /// public bool Match(string s, out RXMatch result, Range? range = null, RXMatchFlags matchFlags = 0) { result = null; int start = _GetSpan(s, range, out var span); int rc = _PcreMatch(span, start, matchFlags, rawFlags: false, out var m, needM: true); if (rc >= 0 || m.mark != null) { result = new RXMatch(this, s, rc, in m); } return rc >= 0; } /// /// Returns true if string s matches this regular expression. /// Gets whole match or some group, as (index, length, value). /// /// ///
• If full match, returns true, and result contains the match or the specified group. ///
• If partial match, returns true. Partial match is possible if used a PARTIAL_ flag. Then cannot get groups, therefore group should be 0. ///
• If no match, returns false, and result is empty. ///
/// /// Subject string. /// If null, returns false, even if the regular expression matches empty string. /// /// /// Group number (1-based index) of result. If 0 - whole match. /// See also . /// /// Invalid group or range. /// The PCRE API function pcre2_match failed. Unlikely. /// /// This function is a simplified version of . /// /// /// /// /// public bool Match(string s, int group, out RXGroup result, Range? range = null, RXMatchFlags matchFlags = 0) { int start = _GetSpan(s, range, out var span); int rc = _PcreMatch(span, start, matchFlags, rawFlags: false, out var m, needM: true, group); if (rc < 0) { result = default; return false; } result = new RXGroup(s, m.vec[group]); return true; } /// /// Returns true if string s matches this regular expression. /// Gets whole match or some group, as string. /// /// ///
• If full match, returns true, and result contains the value of the match or of the specifed group. ///
• If partial match, returns true. Partial match is possible if used a PARTIAL_ flag. Then cannot get groups, therefore group should be 0. ///
• If no match, returns false, and result is null. ///
/// /// Subject string. /// If null, returns false, even if the regular expression matches empty string. /// /// Receives the match value. /// /// /// /// public bool Match(string s, int group, out string result, Range? range = null, RXMatchFlags matchFlags = 0) { result = null; if (!Match(s, group, out RXGroup g, range, matchFlags)) return false; result = g.Value; return true; } /// /// Returns true if string span s matches this regular expression. /// Gets whole match or some group, as . /// /// public bool Match(RStr s, int group, out StartEnd result, Range? range = null, RXMatchFlags matchFlags = 0) { int start = _GetSpan(ref s, range); int rc = _PcreMatch(s, start, matchFlags, rawFlags: false, out var m, needM: true, group); if (rc < 0) { result = default; return false; } result = m.vec[group]; return true; } /// /// Returns true if string span s matches this regular expression. /// Writes match info to caller-allocated memory (array, stackalloc array, etc). /// /// Receives match info: main match in result[0] and group matches in other elements. result.Length must be equal to the number of groups + 1. If a group does not exists, the element's start and end are -1. /// Invalid range. /// The PCRE API function pcre2_match failed. Unlikely. /// result array too short. /// public bool Match(RStr s, Span result, Range? range = null, RXMatchFlags matchFlags = 0) { int start = _GetSpan(ref s, range); int rc = _PcreMatch(s, start, matchFlags, rawFlags: false, out var m, needM: true); if (rc >= 0 || m.mark != null) { if (result.Length < m.vecCount) throw new ArgumentException("result array too short"); new Span(m.vec, m.vecCount).CopyTo(result); } return rc >= 0; } ///// ///// Receives match info. Its "get string value" functions cannot be used. //public bool Match(RStr s, out RXMatch result, Range? range = null, RXMatchFlags matchFlags = 0) { // result = null; // int start = _GetSpan(ref s, range); // int rc = _PcreMatch(s, start, matchFlags, rawFlags: false, out var m, needM: true); // if (rc >= 0 || m.mark != null) { // result = new RXMatch(this, null, rc, in m); // } // return rc >= 0; //} //Used by FindAllX and ReplaceAllX to easily find matches in loop. struct _MatchEnum { regexp _rx; string _subject; Cpp.RegexMatch _m; RXMatchFlags _matchFlags; int _group, _from, _to, _maxCount, _rc; public int foundCount; //Throws if s is null or if invalid start/end or used 'partial' flags. public _MatchEnum(regexp rx, string s, int group, Range? range, RXMatchFlags matchFlags, int maxCount = -1) { Not_.Null(s); (_from, _to) = range.GetStartEnd(s.Length); _rx = rx; _subject = s; _group = group; _matchFlags = rx._GetMatchFlags(matchFlags, throwIfPartial: true); _maxCount = maxCount; foundCount = _rc = 0; _m = default; } //Calls Cpp_RegexMatch, remembers its results, increments foundCount if found. //Returns false if it returns -1. Throws if it returns < -1. Throws if invalid group. //To get results, use properties Match or GroupX. Don't call Next or any other match function before it. public bool Next() { if (foundCount >= (uint)_maxCount) return false; _rc = _rx._PcreMatch(_subject.AsSpan(0, _to), _from, _matchFlags, rawFlags: true, out _m, needM: true, _group); if (_rc < 0) return false; _SetNextFrom(); _matchFlags |= RXMatchFlags.NO_UTF_CHECK; foundCount++; return true; } void _SetNextFrom() { var p = _m.vec[0]; //x=start, y=end _from = p.end; //empty match? if (_from <= p.start) { if (_from < p.start) throw new ArgumentException(@"This function does not support (?=...\K)."); if (++_from < _to) { var c = _subject[_from]; if (c == '\n') { //skip \n if inside \r\n if (_subject[_from - 1] == '\r') _from++; } else if ((c & 0xfc00) == 0xdc00) { //skip the second part of surrogate pair if (0 != (_rx._InfoAllOptions & RXFlags.UTF)) _from++; } } if (_from > _to) _maxCount = 0; } } public RXMatch Match => new RXMatch(_rx, _subject, _rc, in _m); public StartEnd GroupR => _m.vec[_group]; public RXGroup GroupG => new(_subject, GroupR); public string GroupS { get { var r = GroupR; return r.start < 0 ? null : _subject[r.start..r.end]; } } } /// /// Finds all match instances of the regular expression. /// /// A lazy IEnumerable<RXMatch> that can be used with foreach. /// Subject string. Cannot be null. /// s is null. /// Invalid range. /// ///
• Used a PARTIAL_ flag. ///
• The regular expression contains (?=...\K). ///
/// The PCRE API function pcre2_match failed. Unlikely. /// /// This function is similar to . /// /// /// /// /// public IEnumerable FindAll(string s, Range? range = null, RXMatchFlags matchFlags = 0) { var e = new _MatchEnum(this, s, 0, range, matchFlags); while (e.Next()) yield return e.Match; } /// A lazy IEnumerable<string> that can be used with foreach. /// /// Group number (1-based index) of results. If 0 - whole match. /// See also . /// /// s is null. /// Invalid group or range. /// ///
• Used a PARTIAL_ flag. ///
• The regular expression contains (?=...\K). ///
/// The PCRE API function pcre2_match failed. Unlikely. /// /// /// /// public IEnumerable FindAll(string s, int group, Range? range = null, RXMatchFlags matchFlags = 0) { var e = new _MatchEnum(this, s, group, range, matchFlags); while (e.Next()) yield return e.GroupS; } /// A lazy IEnumerable<RXGroup> that can be used with foreach. /// /// /// /// public IEnumerable FindAllG(string s, int group, Range? range = null, RXMatchFlags matchFlags = 0) { var e = new _MatchEnum(this, s, group, range, matchFlags); while (e.Next()) yield return e.GroupG; } /// /// Finds all match instances of the regular expression. Gets array of . /// /// true if found 1 or more matches. /// Receives all found matches. /// /// /// /// public bool FindAll(string s, out RXMatch[] result, Range? range = null, RXMatchFlags matchFlags = 0) { result = FindAll(s, range, matchFlags).ToArray(); return result.Length != 0; } /// /// Finds all match instances of the regular expression. Gets array of strings. /// /// true if found 1 or more matches. /// Receives all found matches. /// /// /// /// public bool FindAll(string s, int group, out string[] result, Range? range = null, RXMatchFlags matchFlags = 0) { result = FindAll(s, group, range, matchFlags).ToArray(); return result.Length != 0; } /// /// Finds all match instances of the regular expression. Gets array of (index, length, value). /// /// true if found 1 or more matches. /// Receives all found matches. /// /// /// /// public bool FindAllG(string s, int group, out RXGroup[] result, Range? range = null, RXMatchFlags matchFlags = 0) { result = FindAllG(s, group, range, matchFlags).ToArray(); return result.Length != 0; } int _Replace(string s, out string result, string repl, Func replFunc, int maxCount, Range? range, RXMatchFlags matchFlags) { StringBuilder b = null; StringBuilder_ bCache = default; int prevEnd = 0; int replType = 0; //0 empty, 1 simple, 2 with $, 3 callback var e = new _MatchEnum(this, s, 0, range, matchFlags, maxCount); while (e.Next()) { //init variables if (b == null) { bCache = new StringBuilder_(out b, s.Length + 100); if (replFunc != null) replType = 3; else if (!repl.NE()) replType = repl.IndexOf('$') < 0 ? 1 : 2; } //append s part before this match var p = e.GroupR; //x=start, y=end int nBefore = p.start - prevEnd; if (nBefore != 0) b.Append(s, prevEnd, nBefore); prevEnd = p.end; //append replacement string re = null; if (replType >= 2) { var m = e.Match; //FUTURE: optimization: if no callback, use single instance and set fields. if (replFunc != null) re = replFunc(m); else ExpandReplacement_(m, repl, b); } else re = repl; if (!re.NE()) b.Append(re); } //append s part after last match if (e.foundCount != 0) { int nAfter = s.Length - prevEnd; if (nAfter > 0) b.Append(s, prevEnd, nAfter); result = b.ToString(); bCache.Dispose(); } else result = s; return e.foundCount; } /// /// Finds and replaces all match instances of the regular expression. /// /// The result string. /// Subject string. Cannot be null. /// /// Replacement pattern. /// Can consist of any combination of literal text and substitutions like $1. /// Supports .NET regular expression substitution syntax. See . Also: replaces $* with the name of the last encountered mark; replaces ${+func} etc with the return value of a function registered with . /// /// Maximal count of replacements to make. If -1 (default), replaces all. /// s is null. /// Invalid range. /// /// - Invalid $replacement. /// - Used a PARTIAL_ flag. /// - The regular expression contains (?=...\K). /// /// The PCRE API function pcre2_match failed. Unlikely. /// /// This function is similar to . /// /// /// /// /// public string Replace(string s, [ParamString(PSFormat.RegexpReplacement)] string repl = null, int maxCount = -1, Range? range = null, RXMatchFlags matchFlags = 0) { _Replace(s, out var R, repl, null, maxCount, range, matchFlags); return R; } /// The number of replacements made. Returns the result string through an out parameter. /// The result string. Can be the same variable as the subject string. /// /// /// /// public int Replace(string s, [ParamString(PSFormat.RegexpReplacement)] string repl, out string result, int maxCount = -1, Range? range = null, RXMatchFlags matchFlags = 0) { return _Replace(s, out result, repl, null, maxCount, range, matchFlags); } /// /// Finds and replaces all match instances of the regular expression. Uses a callback function. /// /// /// Callback function's delegate, eg lambda. Called for each found match. Returns the replacement. /// In the callback function you can use . /// /// /// This function is similar to . /// /// /// o.Value.Upper()); /// print.it(s); /// ]]> /// /// public string Replace(string s, Func replFunc, int maxCount = -1, Range? range = null, RXMatchFlags matchFlags = 0) { _Replace(s, out var R, null, replFunc, maxCount, range, matchFlags); return R; } /// /// Finds and replaces all match instances of the regular expression. Uses a callback function. /// /// /// Callback function's delegate, eg lambda. Called for each found match. Returns the replacement. /// In the callback function you can use . /// /// /// This function is similar to . /// /// /// o.Value.Upper(), out s)) print.it("not found"); /// else print.it(s); /// ]]> /// /// public int Replace(string s, Func replFunc, out string result, int maxCount = -1, Range? range = null, RXMatchFlags matchFlags = 0) { return _Replace(s, out result, null, replFunc, maxCount, range, matchFlags); } /// /// Used by _ReplaceAll and RXMatch.ExpandReplacement. /// Fully supports .NET regular expression substitution syntax. Also: replaces $* with the name of the last encountered mark; replaces ${+func} etc with the return value of a function registered with . /// [MethodImpl(MethodImplOptions.AggressiveOptimization)] internal static void ExpandReplacement_(RXMatch m, string repl, StringBuilder b) { fixed (char* s0fixed = repl) { char* s0 = s0fixed, s = s0, eos = s + repl.Length, e = s; //e is the end of s part added to b while (s < eos) { if (*s == '$') { if (s > e) { b.Append(e, (int)(s - e)); e = s; } char ch = *++s; if (ch == '$') { //escaped $ e = s++; continue; } char* s1e = s; //for errors only int group = -1; if (ch == '{') { //${name} or ${number} char* t = ++s; while (t < eos && *t != '}') t++; if (t == eos) break; ch = *s; if (ch == '+') { //${+userFunc} or ${+userFunc(group)} or ${+userFunc(group, param)} s++; string funcName = null, funcParam = null; int groupNumber = 0; if (t[-1] == ')') { t--; var sArgs = s; while (sArgs < t && *sArgs != '(') sArgs++; if (sArgs < t) { funcName = new(s, 0, (int)(sArgs - s)); var sParam = ++sArgs; while (sParam < t && *sParam != ',') sParam++; groupNumber = _GetGroup(sArgs, sParam); if (groupNumber >= m.GroupCountPlusOne) funcName = null; else if (sParam < t) { if (*++sParam == ' ') sParam++; funcParam = new(sParam, 0, (int)(t - sParam)); } } t++; } else { funcName = new string(s, 0, (int)(t - s)); } if (funcName == null || !s_userReplFuncs.TryGetValue(funcName, out var replFunc)) group = int.MaxValue; else b.Append(replFunc(m, groupNumber, funcParam)); } else group = _GetGroup(s, t); int _GetGroup(char* start, char* end) { if (*start >= '0' && *start <= '9') { //${number}. info: group name cannot start with a digit, then PCRE returns error. int i = repl.ToInt((int)(start - s0), out int numEnd, STIFlags.NoHex); if (s0 + numEnd == end && i >= 0) return i; } else { //${name} int i = m.GroupNumberFromName_(start, (int)(end - start), out _); //speed: 40-100 ns if (i >= 0) return i; } return int.MaxValue; } s = t + 1; } else if (ch >= '0' && ch <= '9') { //$number group = repl.ToInt((int)(s - s0), out int numEnd, STIFlags.NoHex); if (numEnd == 0 || group < 0) group = int.MaxValue; s = s0 + numEnd; } else { s++; if (ch == '`') { //part before match int i = m.Start; if (i > 0) b.Append(m.Subject, 0, i); } else if (ch == '\'') { //part after match var subject = m.Subject; int i = m.End, len = subject.Length - i; if (len > 0) b.Append(subject, i, len); } else if (ch == '&') { //whole match group = 0; } else if (ch == '+') { //last group group = m.GroupCountPlusOne - 1; } else if (ch == '_') { //subject b.Append(m.Subject); } else if (ch == '*') { //last mark b.Append(m.Mark); } else group = int.MaxValue; } if (group >= 0) { //if $invalid, throw exception. Would be harmful to ignore when replacing in multiple files. if (group >= m.GroupCountPlusOne) throw new ArgumentException($"Invalid regex replacement: {new string(--s1e, 0, (int)(s - s1e))}"); var g = m[group]; if (g.Length > 0) b.Append(g.Subject_, g.Start, g.Length); } e = s; } else s++; } int tail = (int)(eos - e); if (tail > 0) b.Append(e, tail); } } /// /// Adds or replaces a function that is called when a regular expression replacement string contains ${+name} or ${+name(g)} or ${+name(g, v)}, where g is group number or name and v is any string. /// /// A string used to identify the function. Can contain any characters except '}', '(' and ')'. /// /// Callback function. Called for each found match. Returns the replacement. /// Parameters: ///
• current match. ///
• group number g, if replacement is like ${+name(g)} or ${+name(g, v)}; else 0. ///
• string v, if replacement is like ${+name(g, v)}; else null. /// /// /// In the callback function you can use . /// /// /// /// Useful when there is no way to use overloads with a replFunc parameter. For example in Find/Replace UI. /// /// /// Create new script in editor and add this code. In Properties set role editorExtension. Run. /// Then in the Find panel in the replacement field you can use ${+Lower}, ${+Lower(1)}, ${+Lower(2)} etc. /// m[g].Value.Lower()); //make lowercase /// ]]> /// Another example. Replacement could be like ${+mul(1, 10)}. /// (m[g].Value.ToInt() * v.ToInt()).ToString()); //multiply by v /// ]]> /// public static void addReplaceFunc(string name, Func replFunc) { s_userReplFuncs[name] = replFunc; } static ConcurrentDictionary> s_userReplFuncs = new(); //rejected: use pcre2_substitute. Not useful because: we cannot implement RXMatch.ExpandReplacement with it; we have addReplaceFunc. /// /// Returns an array of substrings that in the subject string are delimited by regular expression matches. /// /// Subject string. Cannot be null. /// Maximal count of substrings to get. The last substring contains the unsplit remainder of the subject string. If 0 (default) or negative, gets all. /// s is null. /// Invalid range. /// ///
• Used a PARTIAL_ flag. ///
• The regular expression contains (?=...\K). ///
/// The PCRE API function pcre2_match failed. Unlikely. /// /// Element 0 of the returned array is s substring until the first match of the regular expression, element 1 is substring between the first and second match, and so on. If no matches, the array contains single element and it is s. /// /// This function is similar to . /// /// /// /// /// public string[] Split(string s, int maxCount = 0, Range? range = null, RXMatchFlags matchFlags = 0) { if (maxCount < 0) maxCount = 0; if (maxCount != 1) { var a = new List(); int prevEnd = 0; var e = new _MatchEnum(this, s, 0, range, matchFlags, maxCount - 1); while (e.Next()) { var p = e.GroupR; a.Add(s[prevEnd..p.start]); prevEnd = p.end; } if (e.foundCount > 0) { a.Add(s[prevEnd..]); return a.ToArray(); } } return new string[] { s }; } /// /// Returns array of substrings delimited by regular expression matches. /// /// /// /// /// public RXGroup[] SplitG(string s, int maxCount = 0, Range? range = null, RXMatchFlags matchFlags = 0) { if (maxCount < 0) maxCount = 0; if (maxCount != 1) { var a = new List(); int prevEnd = 0; var e = new _MatchEnum(this, s, 0, range, matchFlags, maxCount - 1); while (e.Next()) { var p = e.GroupR; a.Add(new RXGroup(s, prevEnd, p.start)); prevEnd = p.end; } if (e.foundCount > 0) { a.Add(new RXGroup(s, prevEnd, s.Length)); return a.ToArray(); } } return new RXGroup[] { new RXGroup(s, 0, s.Length) }; } //rejected: probably rarely used. Or need IEnumerable too. //public IEnumerable SplitE(string s, int maxCount = 0, Range? range = null, RXMatchFlags matchFlags = 0) //{ // if(maxCount< 0) maxCount = 0; // if(maxCount != 1) { // int prevEnd = 0; // var e = new _MatchEnum(this, s, 0, range, matchFlags, maxCount - 1); // while(e.Next()) { // var p = e.GroupP; // yield return new RXGroup(s, prevEnd, p.start); // prevEnd = p.end; // } // if(e.foundCount > 0) { // yield return new RXGroup(s, prevEnd, s.Length); // yield break; // } // } // yield return new RXGroup(s, 0, s.Length); //} //Calls pcre2_pattern_info(ALLOPTIONS), which returns flags passed to the ctor and possibly modified by (*OPTION) and possibly added UTF if contains non-ASCII characters. //Actually RXFlags is long, where the high 32 bits is extended options. This func gets only the main options (the low 32 bits). RXFlags _InfoAllOptions { get { RXFlags R; Cpp.pcre2_pattern_info(_CodeHR, Cpp.PCRE2_INFO_.ALLOPTIONS, &R); return R; } } /// /// Encloses string in \Q \E if it contains metacharacters \^$.[|()?*+{ or if always == true. /// /// Can be null. /// Enclose always, even if the string does not contain metacharacters. Should be true if the regular expression in which this string will be used has option "extended", because then whitespace is ignored and # is a special character too. /// /// Such enclosed substring in a regular expression is interpreted as a literal string. /// This function also escapes \E, so that it does not end the literal string. /// public static string escapeQE(string s, bool always = false) { if (s == null) return s; if (always) goto g1; for (int i = 0; i < s.Length; i++) { char c = s[i]; if ((c >= '(' && c <= '+') || c == '\\' || c == '.' || c == '?' || c == '{' || c == '[' || c == '|' || c == '$' || c == '^') goto g1; //this is slower //if(c < 128) { // if(c < 64) { // if(0 != (0b1000000000000000010011110001000000000000000000000000000000000000UL & (1UL << c))) goto g1; // } else { // if(0 != (0b0001100000000000000000000000000001011000000000000000000000000000UL & (1UL << (c - 64)))) goto g1; // } //} } return s; g1: return @"\Q" + s.Replace(@"\E", @"\E\\E\Q") + @"\E"; } } ================================================ FILE: Au/String/regexp_ExtString.cs ================================================ namespace Au.Types; public static partial class ExtString { /// /// Returns true if this string matches PCRE regular expression rx. /// /// This string. If null, returns false. /// Invalid regular expression. /// Invalid range. /// Failed (unlikely). /// More info and examples: . /// /// /// public static bool RxIsMatch(this string t, [ParamString(PSFormat.Regexp)] string rx, RXFlags flags = 0, Range? range = null) { var x = _cache.AddOrGet(rx, flags); return x.IsMatch(t, range); } /// /// Returns true if this string matches PCRE regular expression rx. /// Gets match info as . /// /// This string. If null, returns false. /// Invalid range. /// Invalid regular expression. /// Failed (unlikely). /// More info and examples: . /// /// /// public static bool RxMatch(this string t, [ParamString(PSFormat.Regexp)] string rx, out RXMatch result, RXFlags flags = 0, Range? range = null) { var x = _cache.AddOrGet(rx, flags); return x.Match(t, out result, range); } /// /// Returns true if this string matches PCRE regular expression rx. /// Gets whole match or some group, as string. /// /// This string. If null, returns false. /// Invalid group or range. /// Invalid regular expression. /// Failed (unlikely). /// More info and examples: . /// /// /// public static bool RxMatch(this string t, [ParamString(PSFormat.Regexp)] string rx, int group, out string result, RXFlags flags = 0, Range? range = null) { var x = _cache.AddOrGet(rx, flags); return x.Match(t, group, out result, range); } /// /// Returns true if this string matches PCRE regular expression rx. /// Gets whole match or some group, as index and length. /// /// This string. If null, returns false. /// Invalid group or range. /// Invalid regular expression. /// Failed (unlikely). /// More info and examples: . /// /// /// public static bool RxMatch(this string t, [ParamString(PSFormat.Regexp)] string rx, int group, out RXGroup result, RXFlags flags = 0, Range? range = null) { var x = _cache.AddOrGet(rx, flags); return x.Match(t, group, out result, range); } /// /// Finds all match instances of PCRE regular expression rx. /// /// This string. /// Invalid range. /// Invalid regular expression. Or used a PARTIAL_ flag. /// Failed (unlikely). /// More info and examples: . /// /// /// public static IEnumerable RxFindAll(this string t, [ParamString(PSFormat.Regexp)] string rx, RXFlags flags = 0, Range? range = null) { if (t == null) throw new NullReferenceException(); var x = _cache.AddOrGet(rx, flags); return x.FindAll(t, range); } /// /// Finds all match instances of PCRE regular expression rx. Gets array of . /// /// This string. /// Invalid range. /// Invalid regular expression. Or used a PARTIAL_ flag. /// Failed (unlikely). /// More info and examples: . /// /// /// public static bool RxFindAll(this string t, [ParamString(PSFormat.Regexp)] string rx, out RXMatch[] result, RXFlags flags = 0, Range? range = null) { if (t == null) throw new NullReferenceException(); var x = _cache.AddOrGet(rx, flags); return x.FindAll(t, out result, range); } /// /// Finds all match instances of PCRE regular expression rx. /// /// This string. /// Invalid group or range. /// Invalid regular expression. Or used a PARTIAL_ flag. /// Failed (unlikely). /// More info and examples: . /// /// /// public static IEnumerable RxFindAll(this string t, [ParamString(PSFormat.Regexp)] string rx, int group, RXFlags flags = 0, Range? range = null) { if (t == null) throw new NullReferenceException(); var x = _cache.AddOrGet(rx, flags); return x.FindAll(t, group, range); } /// /// Finds all match instances of PCRE regular expression rx. Gets array of strings. /// /// This string. /// Invalid group or range. /// Invalid regular expression. Or used a PARTIAL_ flag. /// Failed (unlikely). /// More info and examples: . /// /// /// public static bool RxFindAll(this string t, [ParamString(PSFormat.Regexp)] string rx, int group, out string[] result, RXFlags flags = 0, Range? range = null) { if (t == null) throw new NullReferenceException(); var x = _cache.AddOrGet(rx, flags); return x.FindAll(t, group, out result, range); } //rejected. Rarely used. ///// ///// Finds all match instances of PCRE regular expression rx. Gets array of . ///// ///// This string. ///// Invalid group or range. ///// Invalid regular expression. Or used a PARTIAL_ flag. ///// Failed (unlikely). ///// More info and examples: . ///// ///// ///// //public static bool RxFindAll(this string t, // [ParamString(PSFormat.regexp)] string rx, // int group, out RXGroup[] result, RXFlags flags = 0, Range? range = null) { // if (t == null) throw new NullReferenceException(); // var x = _cache.AddOrGet(rx, flags); // return x.FindAllG(t, group, out result, range); //} /// /// Finds and replaces all match instances of PCRE regular expression rx. /// /// This string. /// Invalid range. /// /// - Invalid regular expression. /// - Invalid $replacement. /// - Used a PARTIAL_ flag. /// - The regular expression contains (?=...\K). /// /// Failed (unlikely). /// More info and examples: . /// /// /// public static string RxReplace(this string t, [ParamString(PSFormat.Regexp)] string rx, [ParamString(PSFormat.RegexpReplacement)] string repl, int maxCount = -1, RXFlags flags = 0, Range? range = null) { if (t == null) throw new NullReferenceException(); var x = _cache.AddOrGet(rx, flags); return x.Replace(t, repl, maxCount, range); } /// /// Finds and replaces all match instances of PCRE regular expression rx. /// /// This string. /// Invalid range. /// /// - Invalid regular expression. /// - Invalid $replacement. /// - Used a PARTIAL_ flag. /// - The regular expression contains (?=...\K). /// /// Failed (unlikely). /// More info and examples: . /// /// /// public static int RxReplace(this string t, [ParamString(PSFormat.Regexp)] string rx, [ParamString(PSFormat.RegexpReplacement)] string repl, out string result, int maxCount = -1, RXFlags flags = 0, Range? range = null) { if (t == null) throw new NullReferenceException(); var x = _cache.AddOrGet(rx, flags); return x.Replace(t, repl, out result, maxCount, range); } /// /// Finds and replaces all match instances of PCRE regular expression rx. Uses a callback function. /// /// This string. /// Invalid range. /// /// - Invalid regular expression. /// - Invalid $replacement. /// - Used a PARTIAL_ flag. /// - The regular expression contains (?=...\K). /// /// Failed (unlikely). /// More info and examples: . /// /// /// public static string RxReplace(this string t, [ParamString(PSFormat.Regexp)] string rx, Func replFunc, int maxCount = -1, RXFlags flags = 0, Range? range = null) { if (t == null) throw new NullReferenceException(); var x = _cache.AddOrGet(rx, flags); return x.Replace(t, replFunc, maxCount, range); } /// /// Finds and replaces all match instances of PCRE regular expression rx. Uses a callback function. /// /// This string. /// Invalid range. /// /// - Invalid regular expression. /// - Invalid $replacement. /// - Used a PARTIAL_ flag. /// - The regular expression contains (?=...\K). /// /// Failed (unlikely). /// More info and examples: . /// /// /// public static int RxReplace(this string t, [ParamString(PSFormat.Regexp)] string rx, Func replFunc, out string result, int maxCount = -1, RXFlags flags = 0, Range? range = null) { if (t == null) throw new NullReferenceException(); var x = _cache.AddOrGet(rx, flags); return x.Replace(t, replFunc, out result, maxCount, range); } /// /// Returns an array of substrings that in this string are delimited by regular expression matches. /// /// This string. /// Invalid range. /// Invalid regular expression. Or used a PARTIAL_ flag. /// Failed (unlikely). /// More info and examples: . /// /// /// public static string[] RxSplit(this string t, [ParamString(PSFormat.Regexp)] string rx, int maxCount = 0, RXFlags flags = 0, Range? range = null) { if (t == null) throw new NullReferenceException(); var x = _cache.AddOrGet(rx, flags); return x.Split(t, maxCount, range); } static _RegexCache _cache = new(); //Cache of compiled regular expressions. //Can make ~10 times faster when the subject string is short. //The algorithm is from .NET Regex source code. class _RegexCache { struct _RXCode { public string regex; public regexp code; //note: could instead cache only PCRE code (nint), but it makes quite difficult public RXFlags flags; } LinkedList<_RXCode> _list = new(); const int c_maxCount = 15; /// /// If rx/flags is in the cache, returns the cached code. /// Else compiles rx/flags, adds to the cache and returns the code. /// /// /// /// Invalid regular expression. Or failed to compile it for some other reason. public regexp AddOrGet(string rx, RXFlags flags) { lock (this) { int len = rx.Length; for (var x = _list.First; x != null; x = x.Next) { var v = x.Value.regex; if (v.Length == len && v == rx && x.Value.flags == flags) { if (x != _list.First) { _list.Remove(x); _list.AddFirst(x); } return x.Value.code; } } { var code = new regexp(rx, flags); var x = new _RXCode() { code = code, regex = rx, flags = flags }; _list.AddFirst(x); if (_list.Count > c_maxCount) _list.RemoveLast(); //note: now cannot free the PCRE code, because another thread may be using it. GC will do it safely. return code; } } } } } ================================================ FILE: Au/String/regexp_types.cs ================================================ using System.Text.RegularExpressions; //for XML doc links namespace Au.Types { /// /// Regular expression match info. /// Used with class functions and extension methods like . /// /// /// Contains info about a regular expression match found in the subject string: index, length, substring, etc. /// Also contains an array of group matches, as . Groups are regular expression parts enclosed in (), except (?...). /// Group matches can be accessed like array elements. Group 0 is whole match. Group 1 is the first group. See examples. /// /// /// /// A group in the subject string may not exist even if whole match found. Then its property is false, -1, 0, null. /// /// public unsafe class RXMatch { internal RXMatch(regexp rx, string subject, int rc, in Cpp.RegexMatch k) { Mark = k.Mark; if (rc < 0) return; Exists = true; IsPartial = rc == 0; StartNoK = k.indexNoK; _rx = rx; //_subject = subject; var g = _groups = new RXGroup[k.vecCount]; var v = k.vec; for (int i = 0; i < g.Length; i++) { g[i] = new RXGroup(subject, v[i]); } } //string readonly _subject; readonly regexp _rx; readonly RXGroup[] _groups; /// /// Gets the subject string in which this match was found. /// public string Subject => _groups[0].Subject_; /// /// Gets the number of groups in the regular expression, + 1 for the whole match. /// public int GroupCountPlusOne => _groups.Length; /// /// Gets start offset of the match in the subject string. The same as that of group 0 (). /// public int Start => _groups[0].Start; /// /// Gets length of the match in the subject string. The same as that of group 0 (). /// public int Length => _groups[0].Length; /// /// Gets end offset of the match in the subject string ( + ). The same as that of group 0 (). /// public int End => _groups[0].End; /// /// Gets substring of the subject string from to . The same as that of group 0 (). /// public string Value => _groups[0].Value; /// /// Gets span of the subject string from to . The same as that of group 0 (). /// /// /// Unlike , does not create new string. /// public RStr Span => _groups[0].Span; /// /// Returns of group 0. /// public override string ToString() => _groups[0].ToString(); /// /// Gets substring of the subject string from to . The same as that of group 0 (). /// /// /// Use this function instead of with results of functions where subject is ReadOnlySpan. /// /// Must be the same subject string as passed to the function that returned this result. internal string GetValue_(RStr subject) => _groups[0].GetValue_(subject); /// /// Gets span of the subject string from to . The same as that of group 0 (). /// /// /// Use this function instead of with results of functions where subject is ReadOnlySpan. /// /// Must be the same subject string as passed to the function that returned this result. internal RStr GetSpan_(RStr subject) => _groups[0].GetSpan_(subject); /// /// Gets start offset of whole match regardless of \K. /// When the regular expression contains \K, this is less than . /// public int StartNoK { get; private set; } /// /// Gets the name of a found mark, or null. /// /// /// Marks can be inserted in regular expression pattern like (*MARK:name) or (*:name). /// After a full successful match, it is the last mark encountered on the matching path through the pattern. After a "no match" or a partial match, it is the last encountered mark. For example, consider this pattern: "^(*MARK:A)((*MARK:B)a|b)c". When it matches "bc", the mark is A. The B mark is "seen" in the first branch of the group, but it is not on the matching path. On the other hand, when this pattern fails to match "bx", the mark is B. /// public string Mark { get; private set; } /// /// Gets the return value of the call. /// /// /// Can be false only when the function returned false but a mark is available (see ). Otherwise, when the function returns false, it returns null instead of a object. /// When false, all properties except and have undefined values or throw exception. /// public bool Exists { get; private set; } /// /// Returns true if this match is partial. /// Partial match is possible if used a PARTIAL_ flag. /// public bool IsPartial { get; private set; } /// /// Gets group info. Index 0 is whole match. Index 1 is the first group. /// /// 1-based group index, or 0 for whole match. /// Invalid group. Max valid value is . public ref RXGroup this[int group] => ref _groups[group]; /// /// Gets group info of a named group. /// /// /// Group name. /// In regular expression, to set name of group (text), use (?<NAME>text). /// /// Unknown group name. /// /// If multiple groups have this name, prefers the first group that matched ( is true). /// public ref RXGroup this[string groupName] { get { int i = GroupNumberFromName(groupName); if (i < 0) throw new ArgumentException("Unknown group name."); return ref _groups[i]; } } /// /// Finds a named group and returns its 1-based index. Returns -1 if not found. /// /// /// Group name. /// In regular expression, to set name of group (text), use (?<NAME>text). /// /// /// /// If multiple groups have this name, prefers the first group that matched ( is true). /// /// public int GroupNumberFromName(string groupName) { Not_.Null(groupName); fixed (char* p = groupName) return GroupNumberFromName_(p, groupName.Length, out _); } /// /// Finds a named group and returns its 1-based index. Returns -1 if not found. /// /// /// Group name. /// In regular expression, to set name of group (text), use (?<NAME>text). /// /// Receives true if multiple groups have this name. /// /// /// If multiple groups have this name, prefers the first group that matched ( is true). /// /// public int GroupNumberFromName(string groupName, out bool notUnique) { Not_.Null(groupName); fixed (char* p = groupName) return GroupNumberFromName_(p, groupName.Length, out notUnique); } //Used by regexp.ReplaceAll to avoid repl.Substring. internal int GroupNumberFromName_(char* s, int len, out bool notUnique) { notUnique = false; if (len > 32 || len < 1) return -1; int step; ushort* first, last; if (s[len] == '\0') { step = Cpp.pcre2_substring_nametable_scan(_rx._CodeHR, s, &first, &last); } else { var p = stackalloc char[33]; int i; for (i = 0; i < len; i++) p[i] = s[i]; p[i] = '\0'; step = Cpp.pcre2_substring_nametable_scan(_rx._CodeHR, p, &first, &last); } if (step <= 0) return -1; int R = 0; notUnique = last > first; for (; first <= last; first += step) { int r = *first; if (_groups[r].Start >= 0) return r; //return the first that is set if (R == 0) R = r; //if none is set, return the first } return R; } /// /// Returns expanded version of the specified replacement pattern. /// /// /// Replacement pattern. /// Can consist of any combination of literal text and substitutions like $1. /// Supports .NET regular expression substitution syntax. See . Also: replaces $* with the name of the last encountered mark; replaces ${+func} and ${+func(n)} with the return value of a function registered with . /// /// /// - Invalid $replacement. /// - Used a PARTIAL_ flag. /// - The regular expression contains (?=...\K). /// /// /// Works like . /// See also: . /// public string ExpandReplacement(string repl) { if (repl.NE()) return repl; using (new StringBuilder_(out var b)) { regexp.ExpandReplacement_(this, repl, b); return b.ToString(); } } } /// /// Regular expression group match info. /// Used with , and some extension methods. /// /// /// Groups are regular expression parts enclosed in (). Except non-capturing parts, like (?:...) and (?options). A RXGroup variable contains info about a group found in the subject string: index, length, substring. /// /// Some groups specified in regular expression may not exist in the subject string even if it matches the regular expression. For example, regular expression "A(\d+)?B" matches string "AB", but group (\d+) does not exist. Then is false, -1, 0, null. /// /// When a group matches multiple times, the RXGroup variable contains only the last instance. For example, if subject is "begin 12 345 67 end" and regular expression is (\d+ )+, value of group 1 is "67". If you need all instances ("12", "345", "67"), instead use .NET and . Also you can get all instances with . /// /// Examples and more info: , . /// public struct RXGroup { readonly string _subject; readonly int _index; //offset in _subject, or -1 if this group does not exist readonly int _len; //length, or 0 if this group match does not exist internal RXGroup(string subject, int start, int end) { _subject = subject; _index = start; _len = end - start; //note: can be <0 if (?=...\K). It's OK. } internal RXGroup(string subject, StartEnd r) { _subject = subject; _index = r.start; _len = r.Length; //note: can be <0 if (?=...\K). It's OK. } internal string Subject_ => _subject; /// /// Gets start offset of the group match in the subject string. /// public int Start => _index; /// /// Gets length of the group match in the subject string. /// public int Length => _len; /// /// Gets end offset of the group match in the subject string ( + ). /// public int End => _index + _len; /// /// Returns true if the group exists in the subject string, false if does not exist. /// More info in topic. Example in topic. /// /// /// Other ways to detect it: if a group does not exist, its is -1 and is null. /// public bool Exists => _index >= 0; /// /// Gets span of the subject string from to . /// /// default if the group does not exist in the subject string (see ). /// /// Unlike , does not create new string. /// public RStr Span => _len > 0 ? _subject.AsSpan(_index, _len) : (_index < 0 ? default : ""); //_len can be < 0 /// /// Gets substring of the subject string from to . /// /// null if the group does not exist in the subject string (see ). /// /// Creates new string each time. See also . /// public string Value => _len > 0 ? _subject[_index..End] : (_index < 0 ? null : ""); //_len can be < 0 /// /// Returns . /// public override string ToString() => Value; //public override string ToString() => _subject != null ? Value : $"{_index}..{End}"; /// /// Gets substring of the subject string from to . /// Returns null if the group does not exist in the subject string (see ). /// /// /// Use this function instead of with results of functions where subject is ReadOnlySpan. /// /// Must be the same subject string as passed to the function that returned this result. internal string GetValue_(RStr subject) => _len > 0 ? subject[_index..End].ToString() : (_index < 0 ? null : ""); /// /// Gets span of the subject string from to . /// Returns null if the group does not exist in the subject string (see ). /// /// /// Use this function instead of with results of functions where subject is ReadOnlySpan. /// /// Must be the same subject string as passed to the function that returned this result. internal RStr GetSpan_(RStr subject) => _len > 0 ? subject.Slice(_index, _len) : (_index < 0 ? default : ""); /// public static implicit operator Range(RXGroup g) => g.Exists ? g.Start..g.End : default; } #region callout /// /// Managed version of PCRE API struct pcre2_callout_block. /// When you set , your callout function's parameter is of this type. /// /// /// More info in PCRE help topic pcre2callout. /// Most properties are pcre2_callout_block fields as documented in PCRE help. Other properties and methods are easier/safer versions of unsafe fields like offset_vector. /// public unsafe struct RXCalloutData { #pragma warning disable 649 //field never assigned struct pcre2_callout_block { public int version; public readonly int callout_number, capture_top, capture_last; public readonly nint* vec; public readonly char* mark, subject; public readonly nint subject_length; public readonly nint start_match; public readonly nint current_position; public readonly nint pattern_position; public readonly nint next_item_length; public readonly nint callout_string_offset; public readonly nint callout_string_length; public readonly char* callout_string; public readonly int callout_flags; } #pragma warning restore 649 //We use pointer instead of adding pcre2_callout_block fields to this struct. Other ways are not good: // Passing whole block to the final callback by value is slow (104 bytes, tested speed). Also then cannot have Result like now. // With 'in' fast, but then users have to declare lambda parameters like 'in RXCalloutData d'. Now just 'd'. pcre2_callout_block* _p; /// /// Sets the return value of the callout function, as documented in PCRE help topic pcre2callout. /// /// /// Default 0. /// If 1, matching fails at the current point, but the testing of other matching possibilities goes ahead, just as if a lookahead assertion had failed. /// If -1 (PCRE2_ERROR_NOMATCH), the match function returns false (no match). Values less tan -2 are PCRE error codes and cause exception. /// public int Result { set => _p->version = value; internal get => _p->version; } internal RXCalloutData(void* calloutBlock) { _p = (pcre2_callout_block*)calloutBlock; Result = 0; } /// /// Callout number, eg 5 for "(?C5)". /// More info in PCRE help topic pcre2callout. /// public int callout_number => _p->callout_number; /// /// One more than the number of the highest numbered captured group so far. /// More info in PCRE help topic pcre2callout. /// public int capture_top => _p->capture_top; /// /// The number of the most recently captured group. /// More info in PCRE help topic pcre2callout. /// public int capture_last => _p->capture_last; /// /// Flags. /// 1 PCRE2_CALLOUT_STARTMATCH, 2 PCRE2_CALLOUT_BACKTRACK. /// More info in PCRE help topic pcre2callout. /// public int callout_flags => _p->callout_flags; /// /// The offset within the subject string at which the current match attempt started. But depends on \K etc. /// More info in PCRE help topic pcre2callout. /// public int start_match => (int)_p->start_match; /// /// The current offset within the subject string. /// public int current_position => (int)_p->current_position; /// /// The offset in the regular expression to the next item to be matched. /// public int pattern_position => (int)_p->pattern_position; /// /// The length of the next item to be processed in the regular expression. /// More info in PCRE help topic pcre2callout. /// public int next_item_length => (int)_p->next_item_length; /// /// The callout string offset in the regular expression. Used with callouts like "(?C'calloutString')". /// More info in PCRE help topic pcre2callout. /// public int callout_string_offset => (int)_p->callout_string_offset; /// /// The callout string, eg "xyz" for "(?C'xyz')". /// More info in PCRE help topic pcre2callout. /// public string callout_string => _p->callout_string == null ? null : new string(_p->callout_string, 0, (int)_p->callout_string_length); /// /// The most recently passed (*MARK), (*PRUNE), or (*THEN) item in the match, or null if no such items have been passed. /// More info in PCRE help topic pcre2callout. /// public string mark => _p->mark == null ? null : new string(_p->mark); /// /// Gets the start index and length of the specified group in the subject string. /// /// Group number (1-based index). /// group must be > 0 and < . public (int index, int length) Group(int group) { if (group <= 0 || group >= _p->capture_top) throw new ArgumentOutOfRangeException(nameof(group), "Must be > 0 and < capture_top."); var v = _p->vec; int i = (int)v[group *= 2]; return (i, (int)v[group + 1] - i); } /// /// Gets the value (substring) of the specified group. /// /// Group number (1-based index). /// group must be > 0 and < . public string GroupValue(int group) { var (i, len) = Group(group); if (i < 0) return null; if (len == 0) return ""; return new string(_p->subject, i, len); } /// /// Gets the start index and length of the most recently captured group in the subject string. /// public (int index, int length) LastGroup => Group(_p->capture_last); /// /// Gets the value (substring) of the most recently captured group. /// public string LastGroupValue => GroupValue(_p->capture_last); } #endregion #pragma warning disable 1591 //no XML doc /// /// Flags for constructor. /// Documented in PCRE help topic pcre2api. /// /// /// Many options also can be specified in regular expression (RE): /// - These can be anywhere in RE: (?i) CASELESS, (?m) MULTILINE, (?s) DOTALL, (?n) NO_AUTO_CAPTURE, (?x) EXTENDED, (?xx) EXTENDED_MORE, (?J) DUPNAMES, (?U) UNGREEDY. Can be multiple, like (?ms). Can be unset, like (?-i). RE "\Qtext\E" is like RE "text" with flag LITERAL. /// - Instead of ANCHORED can be used \G at the start of RE. Or ^, except in multiline mode. /// - Instead of ENDANCHORED can be used \z at the end of RE. Or $, except in multiline mode. /// - Flag UTF is implicitly added if RE contains non-ASCII characters and there is no flag NEVER_UTF. /// - These must be at the very start and are named like flags: (*UTF), (*UCP), (*NOTEMPTY), (*NOTEMPTY_ATSTART), (*NO_AUTO_POSSESS), (*NO_DOTSTAR_ANCHOR), (*NO_START_OPT). /// - More info in PCRE syntax reference. /// /// Some of RXFlags flags also exist in . You can set them either when calling constructor or when calling functions that have parameter more. You can use different flags for each function call with the same variable. /// [Flags] public enum RXFlags : ulong { /// Match only at the first position ANCHORED = 0x80000000, /// Pattern can match only at end of subject ENDANCHORED = 0x20000000, /// Do not check the pattern for UTF validity NO_UTF_CHECK = 0x40000000, /// Allow empty classes ALLOW_EMPTY_CLASS = 0x00000001, /// Alternative handling of \u, \U, and \x ALT_BSUX = 0x00000002, /// Compile automatic callouts AUTO_CALLOUT = 0x00000004, /// Do caseless matching CASELESS = 0x00000008, /// $ not to match newline at end DOLLAR_ENDONLY = 0x00000010, /// . matches anything including newlines DOTALL = 0x00000020, /// Allow duplicate names for subpatterns DUPNAMES = 0x00000040, /// Ignore white space and # comments EXTENDED = 0x00000080, /// Force matching to be before newline FIRSTLINE = 0x00000100, /// Match unset backreferences MATCH_UNSET_BACKREF = 0x00000200, /// ^ and $ match newlines within data MULTILINE = 0x00000400, /// Lock out UCP, e.g. via (*UCP) NEVER_UCP = 0x00000800, /// Lock out UTF, e.g. via (*UTF) NEVER_UTF = 0x00001000, /// Disable numbered capturing paren-theses (named ones available) NO_AUTO_CAPTURE = 0x00002000, /// Disable auto-possessification NO_AUTO_POSSESS = 0x00004000, /// Disable automatic anchoring for .* NO_DOTSTAR_ANCHOR = 0x00008000, /// Disable match-time start optimizations NO_START_OPTIMIZE = 0x00010000, /// Use Unicode properties for \d, \w, etc UCP = 0x00020000, /// Invert greediness of quantifiers UNGREEDY = 0x00040000, /// /// Fully support Unicode text (case-insensitivity etc). More info in PCRE documentation topic pcre2unicode. /// This flag is implicitly added if regular expression contains non-ASCII characters and there is no flag NEVER_UTF. /// UTF = 0x00080000, /// Lock out the use of \C in patterns NEVER_BACKSLASH_C = 0x00100000, /// Alternative handling of ^ in multiline mode ALT_CIRCUMFLEX = 0x00200000, /// Process backslashes in verb names ALT_VERBNAMES = 0x00400000, //USE_OFFSET_LIMIT = 0x00800000, //used with pcre2_set_offset_limit(), but currently we don't support it /// EXTENDED_MORE = 0x01000000, /// Pattern characters are all literal LITERAL = 0x02000000, /// Enable support for matching invalid UTF MATCH_INVALID_UTF = 0x04000000, /// Alternative extended character class syntax ALT_EXTENDED_CLASS = 0x08000000, //PCRE2_EXTRA_ flags. //EXTRA_ALLOW_SURROGATE_ESCAPES = 0x1L << 32, //not used with UTF-16 //EXTRA_BAD_ESCAPE_IS_LITERAL = 0x2L << 32, //dangerous [EditorBrowsable(EditorBrowsableState.Never)] //bad name MATCH_WORD = 0x4L << 32, [EditorBrowsable(EditorBrowsableState.Never)] //bad name MATCH_LINE = 0x8L << 32, /// Pattern matches "words" EXTRA_MATCH_WORD = 0x4L << 32, /// Pattern matches whole lines EXTRA_MATCH_LINE = 0x8L << 32, //EXTRA_ALT_BSUX //noise //EXTRA_ALLOW_LOOKAROUND_BSK //fbc /// Disable mixed ASCII/non-ASCII case folding EXTRA_CASELESS_RESTRICT = 0x80L << 32, /// \d remains ASCII in UCP mode EXTRA_ASCII_BSD = 0x100L << 32, /// \s remains ASCII in UCP mode EXTRA_ASCII_BSS = 0x200L << 32, /// \w remains ASCII in UCP mode EXTRA_ASCII_BSW = 0x400L << 32, /// POSIX classes remain ASCII in UCP mode EXTRA_ASCII_POSIX = 0x800L << 32, /// [:digit:] and [:xdigit:] POSIX classes remain ASCII in UCP mode EXTRA_ASCII_DIGIT = 0x1000L << 32, /// Disallow callouts in pattern EXTRA_NEVER_CALLOUT = 0x8000L << 32, /// Use Turkish I case folding EXTRA_TURKISH_CASING = 0x10000L << 32, //EXTRA_PYTHON_OCTAL //EXTRA_NO_BS0 //rare //Match API flags. regexp ctor moves them to a field that later is combined with RXMatchFlags when calling the match API. /// Subject string is not the beginning of a line NOTBOL = 0x00000001L << 56, //hi byte of long /// Subject string is not the end of a line NOTEOL = 0x00000002L << 56, /// An empty string is not a valid match NOTEMPTY = 0x00000004L << 56, /// An empty string at the start of the subject is not a valid match NOTEMPTY_ATSTART = 0x00000008L << 56, /// Allow partial soft match. See . PARTIAL_SOFT = 0x00000010L << 56, /// Allow partial hard match. See . PARTIAL_HARD = 0x00000020L << 56, } /// /// Flags for class functions. /// Documented in PCRE help topic pcre2api. /// /// /// These flags also exist in ( constructor flags). You can set them either when calling constructor or when calling other functions. /// [Flags] public enum RXMatchFlags : uint { //These are the same as in RXFlags, and can be used either when compiling or when matching. /// Match only at the first position ANCHORED = 0x80000000, /// Pattern can match only at end of subject ENDANCHORED = 0x20000000, /// Do not check the subject for UTF validity NO_UTF_CHECK = 0x40000000, //These are only for matching. Also added to the hi int of RXFlags. /// Subject string is not the beginning of a line NOTBOL = 0x00000001, /// Subject string is not the end of a line NOTEOL = 0x00000002, /// An empty string is not a valid match NOTEMPTY = 0x00000004, /// An empty string at the start of the subject is not a valid match NOTEMPTY_ATSTART = 0x00000008, /// Allow partial soft match. See . PARTIAL_SOFT = 0x00000010, /// Allow partial hard match. See . PARTIAL_HARD = 0x00000020, //COPY_MATCHED_SUBJECT //this library does not use the PCRE API for wchich would need this //DISABLE_RECURSELOOP_CHECK //for PCRE testing //NO_JIT } #pragma warning restore 1591 internal static unsafe partial class Cpp { /// This and related API are documented in the C++ dll project. [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern nint Cpp_RegexCompile(string rx, nint len, RXFlags flags, out int codeSize, out BSTR errStr); [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern int Cpp_RegexDtor(IntPtr code); /// This and related API are documented in the C++ dll project. internal struct RegexMatch { public StartEnd* vec; public int vecCount; public int indexNoK; public char* mark; public string Mark => mark == null ? null : new(mark, 0, mark[-1]); } internal unsafe delegate int PcreCalloutT(void* calloutBlock, void* param); /// This and related API are documented in the C++ dll project. [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern int Cpp_RegexMatch(HandleRef code, char* s, nint len, nint start, RXMatchFlags flags, PcreCalloutT callout, out RegexMatch m, bool needM, out BSTR errStr); //note: don't use [MarshalAs(UnmanagedType.BStr)] out string errStr, it makes much slower. #region PCRE API //internal enum PCRE2_ERROR_ //{ // PARTIAL = 0, //note: the PCRE API value is -2, but our C++ dll API then returns 0 // NOMATCH = -1, // CALLOUT = -37, // NOMEMORY = -48, // NOUNIQUESUBSTRING = -50, // //others not useful //} [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern int pcre2_pattern_info(HandleRef code, PCRE2_INFO_ what, void* where); [DllImport("AuCpp.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern int pcre2_substring_nametable_scan(HandleRef code, char* name, ushort** first, ushort** last); internal enum PCRE2_INFO_ { ALLOPTIONS = 0, //ARGOPTIONS = 1, //BACKREFMAX = 2, //BSR = 3, CAPTURECOUNT = 4, //FIRSTCODEUNIT = 5, //FIRSTCODETYPE = 6, //FIRSTBITMAP = 7, //HASCRORLF = 8, //JCHANGED = 9, //JITSIZE = 10, //LASTCODEUNIT = 11, //LASTCODETYPE = 12, //MATCHEMPTY = 13, //MATCHLIMIT = 14, //MAXLOOKBEHIND = 15, //MINLENGTH = 16, //NAMECOUNT = 17, //NAMEENTRYSIZE = 18, //NAMETABLE = 19, //NEWLINE = 20, //DEPTHLIMIT = 21, //SIZE = 22, //HASBACKSLASHC = 23, //FRAMESIZE = 24, //HEAPLIMIT = 25, } #endregion } } ================================================ FILE: Au/String/wildcard.cs ================================================ using System.Text.RegularExpressions; namespace Au { /// /// Parses and compares [wildcard expression](xref:wildcard_expression). /// /// /// Used in "find" functions. For example in to compare window name, class name and program. /// The "find" function creates a wildex instance (which parses the wildcard expression), then calls for each item (eg window) to compare some its property text. /// /// /// x.Name.Eqi(name) && x.Date.Eqi(date)); /// } /// /// //This version supports wildcard expressions. /// //null-string arguments are not compared. /// Document Find2(string name, string date) { /// wildex n = name, d = date; //null if the string is null /// return Documents.Find(x => (n == null || n.Match(x.Name)) && (d == null || d.Match(x.Date))); /// } /// /// //Example of calling such function. /// //Find item whose name is "example" (case-insensitive) and date starts with "2017-". /// var item = x.Find2("example", "2017-*"); /// ]]> /// public class wildex { //note: could be struct, but somehow then slower. Slower instance creation, calling methods, in all cases. readonly object _o; //string, regexp, Regex or wildex[]. Tested: getting string etc with '_o as string' is fast. readonly WXType _type; readonly bool _ignoreCase; readonly bool _not; /// /// [Wildcard expression](xref:wildcard_expression). /// Cannot be null (throws exception). /// "" will match "". /// /// Case-sensitive even if there is no **c. /// If wildcardExpression is invalid, don't throw exception; let always return false. /// /// Invalid "**options " or regular expression. public wildex([ParamString(PSFormat.Wildex)] string wildcardExpression, bool matchCase = false, bool noException = false) { Not_.Null(wildcardExpression); var s = wildcardExpression; try { _type = WXType.Wildcard; _ignoreCase = !matchCase; string split = null; if (s is ['*', '*', _, ..]) { for (int i = 2, j; i < s.Length; i++) { switch (s[i]) { case ' ': s = s[(i + 1)..]; goto g1; case 't' or 'r' or 'R' or 'm' when _type == WXType.Wildcard: _type = s[i] switch { 't' => WXType.Text, 'r' => WXType.RegexPcre, 'R' => WXType.RegexNet, _ => WXType.Multi }; break; case 'c': _ignoreCase = false; break; case 'n': _not = true; break; case '(': if (s[i - 1] != 'm') goto ge; for (j = ++i; j < s.Length; j++) if (s[j] == ')') break; if (j == s.Length || j == i) goto ge; split = s[i..j]; i = j; break; default: goto ge; } } ge: throw new ArgumentException("Invalid \"**options \" in wildcard expression."); g1: switch (_type) { case WXType.RegexNet: var ro = _ignoreCase ? (RegexOptions.IgnoreCase | RegexOptions.CultureInvariant) : RegexOptions.CultureInvariant; _o = new Regex(s, ro); return; case WXType.RegexPcre: _o = new regexp(s, _ignoreCase ? RXFlags.CASELESS : 0); return; case WXType.Multi: var a = s.Split(split ?? "||"); var multi = new wildex[a.Length]; for (int i = 0; i < a.Length; i++) multi[i] = new wildex(a[i], !_ignoreCase); _o = multi; return; } } if (_type == WXType.Wildcard && !hasWildcardChars(s)) _type = WXType.Text; _o = s; } catch when (noException) { _type = WXType.Error; } } /// /// Creates new from wildcard expression string. /// If the string is null, returns null. /// /// [Wildcard expression](xref:wildcard_expression). /// Invalid "**options " or regular expression. public static implicit operator wildex([ParamString(PSFormat.Wildex)] string wildcardExpression) { if (wildcardExpression == null) return null; return new wildex(wildcardExpression); } //rejected: ReadOnlySpan. Then cannot use eg .NET Regex. /// /// Compares a string with the [wildcard expression](xref:wildcard_expression) used to create this . Returns true if they match. /// /// String. If null, returns false. If "", returns true if it was "" or "*" or a regular expression that matches "". public bool Match(string s) { if (s == null) return false; bool R = false; switch (_type) { case WXType.Wildcard: R = s.Like(_o as string, _ignoreCase); break; case WXType.Text: R = s.Eq(_o as string, _ignoreCase); break; case WXType.RegexPcre: R = (_o as regexp).IsMatch(s); break; case WXType.RegexNet: R = (_o as Regex).IsMatch(s); break; case WXType.Multi: var multi = _o as wildex[]; //[n] parts: all must match (with their option n applied) int nNot = 0; for (int i = 0; i < multi.Length; i++) { var v = multi[i]; if (v.Not) { if (!v.Match(s)) return _not; //!v.Match(s) means 'matches if without option n applied' nNot++; } } if (nNot == multi.Length) return !_not; //there are no parts without option n //non-[n] parts: at least one must match for (int i = 0; i < multi.Length; i++) { var v = multi[i]; if (!v.Not && v.Match(s)) return !_not; } break; default: //Error return false; } return R ^ _not; } /// /// Returns the text or wildcard string. /// null if is not Text or Wildcard. /// public string Text => _o as string; /// /// Returns the object created from regular expression string. /// null if is not RegexPcre (no option r). /// public regexp RegexPcre => _o as regexp; /// /// Gets the Regex object created from regular expression string. /// null if is not RegexNet (no option R). /// public Regex RegexNet => _o as Regex; /// /// Array of variables, one for each part in multi-part text. /// null if is not Multi (no option m). /// public wildex[] MultiArray => _o as wildex[]; /// /// Gets the type of text (wildcard, regex, etc). /// public WXType TextType => _type; /// /// Is case-insensitive? /// public bool IgnoreCase => _ignoreCase; /// /// Has option n? /// public bool Not => _not; /// public override string ToString() { return _o?.ToString(); } /// /// Returns true if string contains wildcard characters: '*', '?'. /// /// Can be null. public static bool hasWildcardChars(RStr s) { foreach (var c in s) if (c is '*' or '?') return true; return false; } } } namespace Au.Types { public static unsafe partial class ExtString { #region Like /// /// Compares this string with a string that possibly contains wildcard characters. /// Returns true if the strings match. /// /// This string. If null, returns false. If "", returns true if pattern is "" or "*". /// String that possibly contains wildcard characters. Cannot be null. If "", returns true if this string is "". If "*", always returns true except when this string is null. /// Case-insensitive. /// pattern is null. /// /// Wildcard characters: /// /// | Character | Will match | Examples /// | - /// | * | Zero or more of any characters. | "start*", "*end", "*middle*" /// | ? | Any single character. | "date ????-??-??" /// /// There are no escape sequences for * and ? characters. /// /// Uses ordinal comparison, ie does not depend on current culture. /// /// See also: [wildcard expression](xref:wildcard_expression). /// /// /// /// /// #if false //somehow speed depends on dll version. With some versions same as C# code, with some slower. Also depends on string. With shortest strings 50% slower. public static bool Like(this string t, string pattern, bool ignoreCase = false) { if(t == null) return false; fixed (char* pt = t, pw = pattern) return Cpp.Cpp_StringLike(pt, t.Length, pw, pattern.Length, ignoreCase); } #else [MethodImpl(MethodImplOptions.AggressiveOptimization)] public static bool Like(this string t, string pattern, bool ignoreCase = false) { Not_.Null(pattern); int patLen = pattern.Length; if (t == null) return false; if (patLen == 0) return t.Length == 0; if (patLen == 1 && pattern[0] == '*') return true; if (t.Length == 0) return false; fixed (char* str = t, pat = pattern) { return _WildcardCmp(str, pat, t.Length, patLen, ignoreCase ? Tables_.LowerCase : null); } //Microsoft.VisualBasic.CompilerServices.Operators.LikeString() supports more wildcard characters etc. Depends on current culture, has bugs, slower 6-250 times. //System.IO.Enumeration.FileSystemName.MatchesSimpleExpression supports \escaping. Slower 2 - 100 times. } /// [MethodImpl(MethodImplOptions.AggressiveOptimization)] public static bool Like(this RStr t, string pattern, bool ignoreCase = false) { Not_.Null(pattern); int patLen = pattern.Length; if (patLen == 0) return t.Length == 0; if (patLen == 1 && pattern[0] == '*') return true; if (t.Length == 0) return false; fixed (char* str = t, pat = pattern) { return _WildcardCmp(str, pat, t.Length, patLen, ignoreCase ? Tables_.LowerCase : null); } //Microsoft.VisualBasic.CompilerServices.Operators.LikeString() supports more wildcard characters etc. Depends on current culture, has bugs, slower 6-250 times. //System.IO.Enumeration.FileSystemName.MatchesSimpleExpression supports \escaping. Slower 2 - 100 times. } [MethodImpl(MethodImplOptions.AggressiveOptimization)] static bool _WildcardCmp(char* s, char* w, int lenS, int lenW, char* table) { char* se = s + lenS, we = w + lenW; //find '*' from start. Makes faster in some cases. for (; w < we && s < se; w++, s++) { char cS = s[0], cW = w[0]; if (cW == '*') goto g1; if (cW == cS || cW == '?') continue; if ((table == null) || (table[cW] != table[cS])) return false; } if (w == we) return s == se; //w ended? goto gr; //s ended g1: //find '*' from end. Makes "*text" much faster. for (; we > w && se > s; we--, se--) { char cS = se[-1], cW = we[-1]; if (cW == '*') break; if (cW == cS || cW == '?') continue; if ((table == null) || (table[cW] != table[cS])) return false; } //Algorithm by Alessandro Felice Cantatore, http://xoomer.virgilio.it/acantato/dev/wildcard/wildmatch.html //Changes: supports '\0' in string; case-sensitive or not; restructured, in many cases faster. int i = 0; gStar: //info: goto used because C# compiler makes the loop faster when it contains less code w += i + 1; if (w == we) return true; s += i; for (i = 0; s + i < se; i++) { char sW = w[i]; if (sW == '*') goto gStar; if (sW == s[i] || sW == '?') continue; if ((table != null) && (table[sW] == table[s[i]])) continue; s++; i = -1; } w += i; gr: while (w < we && *w == '*') w++; return w == we; //info: Could implement escape sequence ** for * and maybe *? for ?. // But it makes code slower etc. // Not so important. // Most users would not know about it. // Usually can use ? for literal * and ?. // Usually can use regular expression if need such precision. // Then cannot use "**options " for wildcard expressions. // Could use other escape sequences, eg [*], [?] and [[], but it makes slower and is more harmful than useful. //The first two loops are fast, but Eq much faster when !ignoreCase. We cannot use such optimizations that it can. //The slowest case is "*substring*", because then the first two loops don't help. // Then similar speed as string.IndexOf(ordinal) and API FindStringOrdinal. // Possible optimization, but need to add much code, and makes not much faster, and makes other cases slower, difficult to avoid it. } #endif /// /// Calls for each wildcard pattern specified in the argument list until it returns true. /// Returns 1-based index of the matching pattern, or 0 if none. /// /// /// Case-insensitive. /// One or more wildcard strings. The strings cannot be null. /// A string in patterns is null. public static int Like(this string t, bool ignoreCase = false, params ReadOnlySpan patterns) { for (int i = 0; i < patterns.Length; i++) if (t.Like(patterns[i], ignoreCase)) return i + 1; return 0; } #endregion Like } //rejected: struct WildexStruct - struct version of wildex class. Moved to the Unused project. // Does not make faster, although in most cases creates less garbage. /// /// The type of text (wildcard expression) of a variable. /// public enum WXType : byte { /// /// Simple text (option t, or no *? characters and no t, r, R options). /// Text, /// /// Wildcard (has *? characters and no t, r, R options). /// calls . /// Wildcard, /// /// PCRE regular expression (option r). /// calls . /// RegexPcre, /// /// .NET regular expression (option R). /// calls . /// RegexNet, /// /// Multiple parts (option m). /// calls Match for each part (see ) and returns true if all negative (option n) parts return true (or there are no such parts) and some positive (no option n) part returns true (or there are no such parts). /// If you want to implement a different logic, call Match for each element (instead of calling Match for this variable). /// Multi, /// /// The regular expression was invalid, and parameter noException true. /// Error, } } ================================================ FILE: Au/System/CpuUsage.cs ================================================ namespace Au.More; /// /// Gets CPU usage. /// /// /// You can use the static functions (easier) or a CpuUsage instance. A CpuUsage instance must be disposed (use using). /// /// /// /// public sealed class CpuUsage : IDisposable { bool _ofProcess, _started; Handle_ _ph; Handle_[] _aph; long _mcs, _cycles; /// /// Use this constructor to get CPU usage of all processes (sum). /// public CpuUsage() { } /// /// Use this constructor to get CPU usage of a process. /// /// Process id. public CpuUsage(int processId) { _ofProcess = true; _ph = Handle_.OpenProcess(processId); } /// /// Use this constructor to get CPU usage of multiple processes (sum). /// /// Process ids. public CpuUsage(IEnumerable processes) { _ofProcess = true; _aph = processes.Select(o => Handle_.OpenProcess(o)).Where(o => !o.Is0).ToArray(); if (_aph.Length == 0) _aph = null; } /// public void Dispose() { if (_ofProcess) { if (_aph == null) _ph.Dispose(); else for (int i = 0; i < _aph.Length; i++) _aph[i].Dispose(); } GC.SuppressFinalize(this); } /// ~CpuUsage() { Dispose(); } /// /// Starts measuring CPU usage. /// /// false if processId is invalid. public bool Start() { _started = _GetCycles(out _cycles); _mcs = perf.mcs; return _started; } /// /// Ends measuring CPU usage, and gets result. /// Call this after calling and waiting at least 1 ms. Don't call if Start returned false. /// /// CPU usage 0 to 100 %. /// Called without successful . public double Stop() { if (!_started) throw new InvalidOperationException(); _started = false; if (!_GetCycles(out var cycles)) return 0; long mcs = perf.mcs - _mcs; var r = (cycles - _cycles) / _CyclesMcs() / mcs; Debug_.PrintIf(r > 1.1); r = Math.Min(r, 1); r = _ofProcess ? r : 1 - r; return Math.Round(r * 100, 2); } bool _GetCycles(out long r) { if (!_ofProcess) { r = _QIPCT(); return true; } if (_aph == null) return Api.QueryProcessCycleTime(_ph, out r); //tested: succeeds and gets cycles even if the process ended, if handle is valid r = 0; for (int i = _aph.Length; --i >= 0;) { Api.QueryProcessCycleTime(_aph[i], out var v); r += v; } return true; [SkipLocalsInit] static unsafe long _QIPCT() { var a = stackalloc long[64]; long r = 0; for (ushort g = 0, ng = Api.GetActiveProcessorGroupCount(); g < ng; g++) { int size = 64 * 8; Api.QueryIdleProcessorCycleTimeEx(g, ref size, a); for (int i = 0; i < size / 8; i++) r += a[i]; } return r; } } //Gets CPU cycles / microsecond * CPU count. static double _CyclesMcs() { if (s_cycles.timeMeasured == 0) { //JIT-compile _ = perf.mcs; } if (Environment.TickCount64 != s_cycles.timeMeasured) { nint th = process.thisThreadHandle; int tp0 = Api.GetThreadPriority(th); Api.SetThreadPriority(th, Api.THREAD_PRIORITY_TIME_CRITICAL); try { int nCpu = ProcessorCount; double r = 0; for (int i = 0; i < 3; i++) { Api.QueryThreadCycleTime(th, out long t1); long mcs = perf.mcs; while (perf.mcs < mcs + 300) { } Api.QueryThreadCycleTime(th, out long t2); mcs = perf.mcs - mcs; var v = (double)(t2 - t1) * nCpu / mcs; //if(i>0 && Math.Abs(v-r)>1000) print.it($"<>{r}, {v}, {v-r}<>"); if (v > r) r = v; } //if (Math.Abs(22436 - r) > 1000) print.it($"<>cycles={r}<>"); //else print.it($"cycles={r}"); s_cycles = (Environment.TickCount64, r); } finally { Api.SetThreadPriority(th, tp0); } } return s_cycles.result; //To avoid incorrect result when this thread interrupted etc, this func measures 3 times and gets max result. Also sets max thread priority. } static (long timeMeasured, double result) s_cycles; /// /// Gets CPU usage of all processes (sum). /// /// How long to measure, milliseconds. Default 10. Min 1. Calls Thread.Sleep(duration);. /// CPU usage 0 to 100 %. public static unsafe double OfAllProcesses(int duration = 10) { ArgumentOutOfRangeException.ThrowIfLessThan(duration, 1); using var u = new CpuUsage(); u.Start(); Thread.Sleep(duration); return u.Stop(); } /// /// Gets CPU usage of a process. /// /// Process id. /// How long to measure, milliseconds. Default 10. Min 1. Calls Thread.Sleep(duration);. /// CPU usage 0 to 100 %. Returns 0 if processId invalid. public static double OfProcess(int processId, int duration = 10) { ArgumentOutOfRangeException.ThrowIfLessThan(duration, 1); using var u = new CpuUsage(processId); if (!u.Start()) return 0; Thread.Sleep(duration); return u.Stop(); } /// /// Gets CPU usage of multiple processes (sum). /// /// Process ids. /// How long to measure, milliseconds. Default 10. Min 1. Calls Thread.Sleep(duration);. /// CPU usage 0 to 100 %. Returns 0 if all ids invalid. public static double OfProcesses(IEnumerable processes, int duration = 10) { ArgumentOutOfRangeException.ThrowIfLessThan(duration, 1); using var u = new CpuUsage(processes); if (!u.Start()) return 0; Thread.Sleep(duration); return u.Stop(); } /// /// Get the number of logical CPU in the system. /// /// /// Unlike , does not depend on process affinity etc. /// public static unsafe int ProcessorCount { get { int r = 0; for (ushort g = 0, ng = Api.GetActiveProcessorGroupCount(); g < ng; g++) { int n = 0; Api.QueryIdleProcessorCycleTimeEx(g, ref n, null); r += n / 8; } return r; } } } ================================================ FILE: Au/System/ProcessMemory.cs ================================================ namespace Au.More; /// /// Allocates, writes and reads memory in other process. /// /// /// Must be disposed. Example: using(var pm=new ProcessMemory(...)) { ... }. /// public unsafe class ProcessMemory : IDisposable { Handle_ _hproc; HandleRef _HprocHR => new(this, _hproc); /// protected virtual void Dispose(bool disposing) { if (_hproc.Is0) return; if (MemAllocated != default) { var mem = MemAllocated; MemAllocated = default; if (!Api.VirtualFreeEx(_HprocHR, mem)) print.warning("Failed to free process memory. " + lastError.message); } Mem = default; _hproc.Dispose(); } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// ~ProcessMemory() => Dispose(false); /// /// Process handle. /// Opened with access PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE. /// public IntPtr ProcessHandle => _hproc; /// /// Address in that process used by the read and write functions. /// /// /// Most read/write functions of this class don't have a parameter "address in that process". Instead they use Mem, which initially is == . But you can set Mem = any valid address in that process; usually you do it when no memory is allocated by the constructor (nBytes 0). /// The address is invalid in this process. /// public IntPtr Mem { get; set; } /// /// Address of memory allocated in that process. /// /// /// The constructor allocates memory with API VirtualAllocEx if nBytes != 0. Finally Dispose will free it with API VirtualFreeEx. /// The setter normally isn't used; if you set MemAllocated = default, Dispose will not free the memory. /// The address is invalid in this process. /// public IntPtr MemAllocated { get; set; } void _Alloc(int pid, wnd w, int nBytes, bool noException) { string err; const uint fl = Api.PROCESS_VM_OPERATION | Api.PROCESS_VM_READ | Api.PROCESS_VM_WRITE; _hproc = w.Is0 ? Handle_.OpenProcess(pid, fl) : Handle_.OpenProcess(w, fl); if (_hproc.Is0) { err = "Failed to open process handle."; goto ge; } if (nBytes != 0) { Mem = MemAllocated = Api.VirtualAllocEx(_HprocHR, default, nBytes); if (MemAllocated == default) { err = "Failed to allocate process memory."; goto ge; } } return; ge: if (noException) Dispose(); else { var e = new AuException(0, err); Dispose(); throw e; } } /// /// Opens process handle and optionally allocates memory in that process. /// /// Process id. /// If not 0, allocates memory of this size in that process. /// Don't throw exceptions. If failed, sets = default. /// Failed to open process handle (usually because of [](xref:uac)) or allocate memory. public ProcessMemory(int processId, int nBytes, bool noException = false) { _Alloc(processId, default, nBytes, noException); } /// /// Opens process handle and optionally allocates memory in that process. /// /// A window of that process. /// If not 0, allocates memory of this size in that process. /// Don't throw exceptions. If failed, sets = default. /// w invalid. /// Failed to open process handle or allocate memory. public ProcessMemory(wnd w, int nBytes, bool noException = false) { if (!noException) w.ThrowIfInvalid(); _Alloc(0, w, nBytes, noException); } /// /// Copies a string from this process to that process (memory address ). /// In that process writes the string as '\0'-terminated char string (UTF-16). /// /// false if failed. /// A string in this process. /// Offset in . /// /// In that process is used (s.Length+1)*2 bytes of memory (+1 for the '\0', * 2 because UTF-16 character size is 2 bytes). /// public bool WriteCharString(string s, int offsetBytes = 0) { if (Mem == default) return false; if (s.NE()) return true; fixed (char* p = s) { return Api.WriteProcessMemory(_HprocHR, Mem + offsetBytes, p, (s.Length + 1) * 2, null); } } /// /// Copies a string from this process to that process (memory address ). /// In that process writes the string as '\0'-terminated byte string. /// /// false if failed. /// A string in this process. Normal C# string (UTF-16). /// Offset in . /// Encoding for converting char string to byte string. If null, uses (UTF-8). public bool WriteByteString(string s, int offsetBytes = 0, Encoding enc = null) { if (Mem == default) return false; if (s.NE()) return true; enc ??= Encoding.Default; var a = enc.GetBytes(s).InsertAt(-1); fixed (byte* p = a) { return Api.WriteProcessMemory(_HprocHR, Mem + offsetBytes, p, a.Length, null); } } [SkipLocalsInit] string _ReadString(bool ansiString, int nChars, int offsetBytes, bool findLength, Encoding enc = null) { if (Mem == default) return null; int n = nChars + 1; if (!ansiString) n *= 2; //bytes with '\0' using FastBuffer b = new(n); if (!Api.ReadProcessMemory(_HprocHR, Mem + offsetBytes, b.p, n, null)) return null; if (findLength) nChars = ansiString ? Ptr_.Length(b.p, nChars) : Ptr_.Length((char*)b.p, nChars); enc ??= Encoding.Default; return ansiString ? new((sbyte*)b.p, 0, nChars, enc) : new((char*)b.p, 0, nChars); } /// /// Copies a char string from that process (memory address ) to this process. /// In that process the string must be in Unicode UTF-16 format. /// /// The copied string, or null if failed. /// Number of characters to copy, not including the terminating '\0'. In both processes a character is 2 bytes. /// Offset in . /// Find string length by searching for '\0' character in length range. If false, the returned string is of length length even if contains '\0' characters. public string ReadCharString(int length, int offsetBytes = 0, bool findLength = false) { return _ReadString(false, length, offsetBytes, findLength); } /// /// Copies a byte string from that process (memory address ) to this process. /// In that process the string must be array of bytes (not Unicode UTF-16). /// /// The copied string, or null if failed. /// Number bytes to copy, not including the terminating '\0'. In that process a character is 1 or more bytes (depending on encoding). In this process will be 2 bytes (normal C# string). /// Offset in . /// Find string length by searching for '\0' character in length range. /// Encoding for converting byte string to char string. If null, uses (UTF-8). public string ReadByteString(int length, int offsetBytes = 0, bool findLength = false, Encoding enc = null) { return _ReadString(true, length, offsetBytes, findLength, enc); } /// /// Copies memory from this process to that process (memory address ). /// /// false if failed. /// Address of memory in this process. /// Number of bytes to copy. /// Offset in . public bool Write(void* ptrFrom, int nBytes, int offsetBytes = 0) { if (Mem == default) return false; return Api.WriteProcessMemory(_HprocHR, Mem + offsetBytes, ptrFrom, nBytes, null); } /// /// Copies memory from this process to a known address in that process. /// /// false if failed. /// Address of memory in that process. /// Address of memory in this process. /// Number of bytes to copy. public bool Write(IntPtr ptrTo, void* ptrFrom, int nBytes) { return Api.WriteProcessMemory(_HprocHR, ptrTo, ptrFrom, nBytes, null); } /// /// Copies memory from that process (memory address ) to this process. /// /// false if failed. /// Address of memory in this process. /// Number of bytes to copy. /// Offset in . public bool Read(void* ptrTo, int nBytes, int offsetBytes = 0) { if (Mem == default) return false; return Api.ReadProcessMemory(_HprocHR, Mem + offsetBytes, ptrTo, nBytes, null); } /// /// Copies memory from a known address in that process to this process. /// /// false if failed. /// Address of memory in that process. /// Address of memory in this process. /// Number of bytes to copy. public bool Read(IntPtr ptrFrom, void* ptrTo, int nBytes) { return Api.ReadProcessMemory(_HprocHR, ptrFrom, ptrTo, nBytes, null); } } ================================================ FILE: Au/System/computer.cs ================================================ //FUTURE: GetCpuUsage. using Microsoft.Win32; namespace Au { /// /// Computer shutdown etc. /// public static unsafe class computer { /// /// Gets the number of milliseconds elapsed since Windows startup, not including the time when the computer sleeps or hibernates. /// To get time with sleep, use . /// /// /// Uses API QueryUnbiasedInterruptTime. /// Uses the low-resolution system timer. Its period usually is 15.25 ms. /// Independent of computer clock time changes. /// public static long tickCountWithoutSleep { get { if (!Api.QueryUnbiasedInterruptTime(out long t)) return Api.GetTickCount64(); return t / 10000; } } //public static void setTime(DateTime time) { //} /// /// Initiates computer shutdown or restart operation. /// /// false if failed. Supports . /// Reboot. /// Don't allow to cancel. Applications with unsaved changes will be forcibly closed. /// The length of time to display the shutdown dialog box, in seconds. /// Display this text in the shutdown dialog box and write to the event log. /// The network name of the computer to be shut down. If null (default), shuts down this computer. If used, this process must be admin. /// /// Calls API InitiateSystemShutdown. /// public static bool shutdown(bool restart = false, bool force = false, int timeoutS = 0, string message = null, string computer = null) { SecurityUtil.SetPrivilege("SeShutdownPrivilege", true); if (!computer.NE()) SecurityUtil.SetPrivilege("SeRemoteShutdownPrivilege", true, computer); return Api.InitiateSystemShutdown(computer, message, timeoutS, force, restart); } /// /// Initiates computer shutdown or restart operation. /// /// false if failed. Supports . /// ExitWindowsEx parameter uFlags. /// ExitWindowsEx parameter dwReason. /// /// Calls API ExitWindowsEx. /// public static bool shutdown(int flags, uint reason = 0) { SecurityUtil.SetPrivilege("SeShutdownPrivilege", true); return Api.ExitWindowsEx(flags, reason); } /// /// Initiates computer logoff (sign out) operation. /// /// false if failed. Supports . /// Don't allow to cancel. Applications with unsaved changes will be forcibly closed. public static bool logoff(bool force = false) { SecurityUtil.SetPrivilege("SeShutdownPrivilege", true); return Api.ExitWindowsEx(force ? 4 : 16, 0); } /// /// Computer sleep, hibernate or monitor off. /// /// false if failed. Supports . /// /// /// To sleep or hibernate uses API SetSuspendState. To turn off display uses WM_SYSCOMMAND. /// /// The SetSuspendState behavior is undefined if the system does not support S1-S3 sleep or S4 hibernate power states. It may fail or use hibernation instead of sleep. About power states: System Power States. Available sleep states: run.console("powercfg.exe", "/A"); /// public static bool suspend(CSuspend how) { if (how == CSuspend.SleepOrDisplay) how = 0 != Api.IsPwrSuspendAllowed() ? CSuspend.Sleep : CSuspend.Display; if (how == CSuspend.Display) { var w = WndUtil.CreateWindowDWP_(messageOnly: true); Api.DefWindowProc(w, Api.WM_SYSCOMMAND, Api.SC_MONITORPOWER, 2); Api.DestroyWindow(w); return true; } SecurityUtil.SetPrivilege("SeShutdownPrivilege", true); return 0 != Api.SetSuspendState((byte)(how == CSuspend.Hibernate ? 1 : 0), 0, 0); //documented: parameter bForce has no effect. } /// /// Initiates computer lock operation. /// /// false if failed. Supports . /// /// Uses API LockWorkStation. /// public static bool lockOrSwitchUser() { //if (switchUser) return Api.WTSDisconnectSession(default, -1, false); //on Win10 the same as lock return Api.LockWorkStation(); } /// /// Returns true if the computer is using battery power. /// /// public static bool isOnBattery => SystemInformation.PowerStatus.PowerLineStatus == System.Windows.Forms.PowerLineStatus.Offline; //first time 4 ms //public static bool isOnBattery => System.Windows.SystemParameters.PowerLineStatus == System.Windows.PowerLineStatus.Offline; //first time 21 ms //FUTURE: events desktopSwitchEvent, sleepEvent. Like SystemEvents. //public static event Action desktopSwitchEvent { // add { // } // remove { // } //} //public static void waitForDesktop(Seconds timeout, bool normalDesktop) { //normal, UAC, lock, screensaver, etc // //using var hook = new WinEventHook(EEvent.SYSTEM_DESKTOPSWITCH); //} #region suspendResumeEvent static Action _srAction; static object _srLock = new(); static IntPtr _srHandle; /// /// When the computer is about to enter a suspended state (sleep or hibernate) or has resumed operation after being suspended. /// /// /// Many system events are available in class. For suspend/resume notifications could be used , but it does not work on most computers. Use this event instead. /// /// The event handler is executed in other thread. The parameter can be only Resume or Suspend. See API PBT_APMSUSPEND and PBT_APMRESUMESUSPEND. /// public static event Action suspendResumeEvent { add { lock (_srLock) { if (osVersion.minWin8) { if (_srHandle == default) { Api.DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS p = new() { Callback = &_SRCallback }; _srHandle = Api.RegisterSuspendResumeNotification((IntPtr)(&p), 2); if (_srHandle == default) throw new AuException(0); } } else { SystemEvents.PowerModeChanged += _SystemEvents_PowerModeChanged; } _srAction += value; } } remove { lock (_srLock) { _srAction -= value; if (osVersion.minWin8) { if (_srAction == null && _srHandle != default) { Api.UnregisterSuspendResumeNotification(_srHandle); _srHandle = default; } } else { SystemEvents.PowerModeChanged -= _SystemEvents_PowerModeChanged; } } } } [UnmanagedCallersOnly] static int _SRCallback(void* context, int type, void* setting) { if (type is 4 or 7) _srAction?.Invoke(type == 4 ? PowerModes.Suspend : PowerModes.Resume); return 0; } static void _SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e) { if (e.Mode is PowerModes.Suspend or PowerModes.Resume) _srAction?.Invoke(e.Mode); } #endregion } } namespace Au.Types { /// /// Used with . /// public enum CSuspend { /// Sleep (power state S1-S3). If these power states unavailable, the function may hibernate instead. Sleep, /// Hibernate. Hibernate, /// Turn off display. It should activate Modern Suspend S0 if available. Display, /// Sleep if power states S1-S3 available, else turn off display (it should activate Modern Suspend S0 if available). SleepOrDisplay, } } ================================================ FILE: Au/System/consoleProcess.cs ================================================ namespace Au; /// /// Runs a console program in hidden mode. Gets its output text and can write input text. /// /// /// Must be disposed. In the example the using statement does it. /// /// /// /// <_>{s}<>"); /// } else { /// if (s == "User: ") c.Write("A"); /// else if (s == "Password: ") c.Write("B"); /// //else if (c.Wait()) continue; //let next Read wait for more text and get old + new text. Use this if other prompts are not possible. /// else if (c.Wait(500)) continue; //wait for more text max 500 ms. If received, let next Read get old + new text. /// else if (dialog.showInput(out var s1, null, s)) c.Write(s1); /// //else print.it($"<><_>{s}<>"); /// else throw new OperationCanceledException(); /// } /// } /// if (c.ExitCode is int ec && ec != 0) throw new Exception($"Failed. Exit code: {ec}"); /// ]]> /// /// public sealed unsafe class consoleProcess : IDisposable { Handle_ _hProcess, _hOutRead, _hInWrite; Decoder _decoder; byte[] _b; char[] _c; int _n, _i; bool _skipN; StringBuilder _sb; /// /// Starts specified console program. /// /// /// Failed, for example file not found. public consoleProcess(string exe, string args = null, string curDir = null) { //rejected: bool separateError (separete stderr from stdout). Why: // Impossible to make it sync with stdout. // Complicated library code. // Comlicated user code. // Rarely used. And because of the above reasons would be very very rarely used. exe = run.NormalizeFile_(true, exe, out _, out _); var ps = new ProcessStarter_(exe, args, curDir, rawExe: true); Handle_ hInRead = default, hOutWrite = default/*, hErrWrite = default*/; Api.PROCESS_INFORMATION pi = default; try { var sa = new Api.SECURITY_ATTRIBUTES(null) { bInheritHandle = 1 }; if (!Api.CreatePipe(out _hOutRead, out hOutWrite, sa, 0)) throw new AuException(0); //if (!Api.CreatePipe(out _hErrRead, out hErrWrite, sa, 0)) throw new AuException(0); if (!Api.CreatePipe(out hInRead, out _hInWrite, sa, 0)) throw new AuException(0); Api.SetHandleInformation(_hInWrite, 1, 0); //remove HANDLE_FLAG_INHERIT Api.SetHandleInformation(_hOutRead, 1, 0); //Api.SetHandleInformation(_hErrRead, 1, 0); ps.si.dwFlags |= Api.STARTF_USESTDHANDLES | Api.STARTF_USESHOWWINDOW; ps.si.hStdInput = hInRead; ps.si.hStdOutput = hOutWrite; //ps.si.hStdError = hErrWrite; ps.si.hStdError = hOutWrite; ps.flags |= Api.CREATE_NEW_CONSOLE; if (!ps.StartL(out pi, inheritHandles: true)) { Dispose(); throw new AuException(0); } _hProcess = pi.hProcess; TerminateFinally = true; process.thisProcessExit += _OnExit; } finally { hInRead.Dispose(); hOutWrite.Dispose(); //hErrWrite.Dispose(); pi.hThread.Dispose(); Ended = _hProcess.Is0; } } /// public void Dispose() { //print.it(Api.WaitForSingleObject(_hProcess, 0)); //info: all tested processes ended after 0-5 ms after ERROR_BROKEN_PIPE if (TerminateFinally && !Ended && Api.WaitForSingleObject(_hProcess, 100) != Api.WAIT_TIMEOUT) TerminateFinally = false; _Dispose(); GC.SuppressFinalize(this); } void _Dispose() { if (_hOutRead.Is0) return; process.thisProcessExit -= _OnExit; if (TerminateFinally && !Ended) TerminateNow(); _hProcess.Dispose(); _hInWrite.Dispose(); _hOutRead.Dispose(); //_hErrRead.Dispose(); } /// ~consoleProcess() { print.warning("consoleProcess not disposed", -1); _Dispose(); } void _OnExit(Exception e) { if (TerminateFinally && !Ended) TerminateNow(); } /// /// Closes the standard input stream (stdin), signaling end of input to the process. /// /// /// Some console programs expect you to close stdin after writing all input data (see ). This signals "end of file" (EOF). /// public void EndInput() { _hInWrite.Dispose(); } /// /// Console's text encoding. /// Default is . /// /// /// If wrong encoding, the received text may contain garbage. Try or . /// public Encoding Encoding { get => _encoding ??= Encoding.UTF8; set { if (value != _encoding) { _encoding = value; _decoder = null; } } } Encoding _encoding; /// /// Input text encoding for . If null (default), will use . /// public Encoding InputEncoding { get; set; } /// /// Waits and reads next full line. /// /// Receives the text. It does not contain newline characters ('\r', '\n'). /// false if there is no more text to read because the console process ended or is ending. /// Failed. /// /// Waits for new text from console, reads it into an internal buffer, and then returns it one full line at a time. /// /// To read a prompt (incomplete line that asks for user input), use instead. This function would just hang waiting for full line ending with newline characters. /// public bool ReadLine(out string s) => _Read(out s, true); /// /// Waits and reads next full or partial line. /// /// Receives the text. It does not contain newline characters ('\r', '\n'). /// false if there is no more text to read because the console process ended or is ending. /// Failed. /// /// Waits for new text from console, reads it into an internal buffer, and then returns it one line at a time. /// /// Sets = true if the line text in the buffer is terminated with newline characters. Else sets IsLine = false. /// /// When IsLine is false, s text can be either a prompt (incomplete line that asks for user input) or an incomplete line/prompt text (the remainder will be retrieved later). Your script should somehow distinguish it. If it's a known prompt, let it call . Else call . See example. /// /// It's impossible to automatically distinguish a prompt from a partial line or partial prompt. The console program can write line text in parts, possibly with delays in between. Or it can write a prompt text and wait for user input. Also this function may receive line text in parts because of limited buffer size etc. /// /// public bool Read(out string s) => _Read(out s, false); /// /// Returns: ///
• 1 - there is data available to read. ///
• 0 - no data. ///
• -1 - error, eg process ended. Supports . ///
internal int CanReadNow_ => _i < _n ? 1 : !Api.PeekNamedPipe(_hOutRead, null, 0, out _, out int n) ? -1 : n > 0 ? 1 : 0; internal IntPtr OutputHandle_ => _hOutRead; bool _ReadPipe() { //native console API allows any buffer size when writing, but wrappers usually use small buffer, eg .NET 4-5 KB, msvcrt 5 KB, C++ not tested _b ??= new byte[8000]; _c ??= new char[_b.Length + 10]; _sb ??= new(); _n = _i = 0; for (; ; ) { IntPtr hRead = _hOutRead; //IsError = hRead == _hErrRead; if (!Api.ReadFileArr(hRead, _b, out int nr)) return false; if (nr > 0) { var b = _b.AsSpan(0, nr); //BOM? Noticed with: robocopy /unicode (UTF-16 BOM followed by ASCII). if (b is [0xEF, 0xBB, 0xBF, ..]) b = b[3..]; else if (b is [0xFF, 0xFE, ..]) b = b[2..]; if (_skipN && _b[0] == 10) { _skipN = false; b = b[1..]; } _decoder ??= Encoding.GetDecoder(); //ensures we'll not get partial multibyte chars (UTF8 etc) at buffer end/start _n = _decoder.GetChars(b, _c, false); if (_n > 0) return true; } } } bool _Read(out string text, bool needLine, Func prompt = null) { text = null; if (Ended) return _R(false); if (_wasReadMore) _wasReadMore = false; else _sb?.Clear(); readPipe: if (_i >= _n) { if (!_ReadPipe()) return _R(_Ended(ref text)); } var k = new Span(_c, _i, _n - _i); int i = k.IndexOfAny('\n', '\r'); if (i < 0) { _sb.Append(k); _n = 0; if (!needLine) { switch (_PeekWait(30, 1)) { case _PWResult.OK: goto readPipe; case _PWResult.End: return _R(_Ended(ref text)); } var s1 = _sb.ToString(); if (prompt != null) { if (prompt(s1)) { _sb.Clear(); return _R(_ReturnText(ref text, s1, false)); } if (_PeekWait(5000, 1) == _PWResult.OK) goto readPipe; text = s1; //for exception message return false; } else { return _R(_ReturnText(ref text, s1, false)); } } goto readPipe; } string s = _sb.Length > 0 ? _sb.Append(k[0..i]).ToString() : k[0..i].ToString(); if (k[i++] == '\r') { if (i == k.Length) _skipN = true; else if (k[i] == '\n') i++; } _i += i; return _R(_ReturnText(ref text, s, true)); bool _ReturnText(ref string text, string s, bool isLine) { text = s; IsLine = isLine; if (isLine) _sb.Clear(); return true; } bool _Ended(ref string text) { if (lastError.code != Api.ERROR_BROKEN_PIPE) throw new AuException(0); Ended = true; if (_sb.Length == 0) return false; return _ReturnText(ref text, _sb.ToString(), true); } bool _R(bool r, [CallerLineNumber] int l_ = 0) { //print.it("return", l_); return r; } } /// /// Waits for more text and tells next to get old + new text. /// /// Timeout, ms. The function returns false if did not receive more text during that time. If -1, returns true without waiting (next will wait). /// true if received more text or if timeout is -1. /// true. Or multiple without . /// /// If returns true, next will get the old text + new text. If the console process ends while waiting, next Read will get the old text, and will be true. /// public bool Wait(int timeout = -1) { if (IsLine) throw new InvalidOperationException("IsLine true"); if (_wasReadMore) throw new InvalidOperationException("multiple Wait without Read"); if (timeout < 0) { if (timeout != -1) throw new ArgumentException(); } else { if (_PeekWait(timeout, 15) == _PWResult.Timeout) return false; //let next _Read clear _sb } return _wasReadMore = true; //let next _Read don't clear _sb but set _wasReadMore = false } bool _wasReadMore; /// /// Waits for next prompt (incomplete line that asks for user input). Reads the prompt and all lines before it. Then can write input text and "\n". /// /// Prompt text. Format: [wildcard expression](xref:wildcard_expression). /// Input text. If "", writes just "\n". If null, does not write. /// List of lines before the prompt. The last item is the prompt. /// Next prompt text does not match prompt (after waiting 5 s for full prompt). Or the console process ended. Or failed to write input. /// /// /// o.Contains("keyword")) ? "A" : "B"); /// ]]> /// "; /// c.Prompt(prompt, "example.exa"); /// foreach (var s in c.Prompt(prompt).SkipLast(1)) print.it(s); /// c.Write("exit"); /// ]]> /// public List Prompt(string prompt, string input = null) { wildex wild = prompt; bool ok = false; Func f = s => ok = wild.Match(s); List a = new(); while (!ok) { if (!_Read(out var s, false, f)) throw new AuException(Ended ? $"The console process ended while waiting for prompt \"{prompt}\"." : $"The prompt text does not match \"{prompt}\". It is \"{s}\"."); a.Add(s); } if (input != null) Write(input); return a; } /// /// Sends text to the console's input. Also sends character '\n' (like key Enter), unless text ends with '\n' or noNL is true. /// /// /// Don't append character '\n' when text does not end with '\n'. /// Failed. public void Write(string text, bool noNL = false) { if (!text.NE()) _Write(text); if (!noNL && text is not [.., '\n']) _Write("\n"); void _Write(string s) { bool ok = Api.WriteFile2(_hInWrite, (InputEncoding ?? _encoding ?? Encoding.UTF8).GetBytes(s), out _); if (!ok) throw new AuException(0); } } /// /// sets this property = true if in console output the line text ended with newline characters; false if not. /// /// /// If returns false, the text returned by the last is either a prompt (incomplete line that asks for user input) or an incomplete line. You can use to wait for more text. /// public bool IsLine { get; private set; } ///// ///// Returns true if the last ReadX function retrieved text from the standard error stream; false if from the standard output stream. ///// //public bool IsError { get; private set; } /// /// Returns true if a ReadX function detected that the console output stream is closed. The process is ended or ending. /// public bool Ended { get; private set; } /// /// Gets the exit code of the console process. /// If the process is still running, waits until it exits. /// /// If fails, returns int.MinValue. public int ExitCode { get { bool retry = false; g1: if (!Api.GetExitCodeProcess(_hProcess, out int r)) return int.MinValue; if (r == 259 && !retry && (retry = 0 == Api.WaitForSingleObject(_hProcess, -1))) goto g1; //STILL_ACTIVE return r; } } /// /// Terminates the console process. /// /// public void TerminateNow(int exitCode = -1) { Api.TerminateProcess(_hProcess, exitCode); TerminateFinally = false; } /// /// If the console process is still running when this variable is dying, terminate it. /// Default true. /// public bool TerminateFinally { get; set; } _PWResult _PeekWait(int time, int minPeriod, int maxPeriod = 100) { for (int period = minPeriod; time > 0;) { if (period > 0) { period.ms(); time -= period; } if (period < maxPeriod) period++; if (!Api.PeekNamedPipe(_hOutRead, null, 0, out _, out int nr)) return _PWResult.End; if (nr > 0) return _PWResult.OK; } return _PWResult.Timeout; } enum _PWResult { OK, Timeout, End } /// /// Reads all console output text until its process ends. Returns that text. /// /// /// Does not parse lines. Does not normalize newline characters. /// /// Failed. public string ReadAllText() { while (_ReadPipe()) _sb.Append(_c, 0, _n); if (lastError.code != Api.ERROR_BROKEN_PIPE) throw new AuException(0); var s = _sb.ToString(); _sb.Clear(); return s; } /// /// Reads all console output text until its process ends. Calls callback function. /// /// Called for each text chunk received from console. /// /// Does not parse lines. Does not normalize newline characters. /// /// Failed. public void ReadAllText(Action output) { if (_sb?.Length > 0) { var s = _sb.ToString(); _sb.Clear(); output(s); } while (_ReadPipe()) output(new(_c, 0, _n)); if (lastError.code != Api.ERROR_BROKEN_PIPE) throw new AuException(0); } } ================================================ FILE: Au/System/osVersion.cs ================================================ namespace Au; /// /// Provides Windows version info. Also some info about the current process (eg 32/64 bit) and this library. /// /// /// The Windows version properties return the true Windows version; it does not depend on manifest etc. /// /// /// public static unsafe class osVersion { static osVersion() { Api.RTL_OSVERSIONINFOW x = default; x.dwOSVersionInfoSize = sizeof(Api.RTL_OSVERSIONINFOW); Api.RtlGetVersion(ref x); //use this because Environment.OSVersion.Version (GetVersionEx) lies, even if we have correct manifest when is debugger present _winver = Math2.MakeWord(_winminor = (int)x.dwMinorVersion, _winmajor = (int)x.dwMajorVersion); _winbuild = (int)x.dwBuildNumber; _minWin8 = _winver >= win8; _minWin8_1 = _winver >= win8_1; _minWin10 = _winver >= win10; if (_minWin10) _win10build = _winbuild; //print.it(_win10build); //this is to remind to add new members for new Windows 10/11 versions //Debug_.PrintIf(_win10build > 19044, $"{_win10build} {Microsoft.Win32.Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion", "DisplayVersion", "failed")}"); _is32BitOS = sizeof(nint) == 4 && !(Api.IsWow64Process(Api.GetCurrentProcess(), out _isWow64) && _isWow64); } static readonly int _winmajor, _winminor, _winver, _winbuild, _win10build; static readonly bool _minWin8, _minWin8_1, _minWin10; static readonly bool _is32BitOS, _isWow64; /// /// Gets Windows major version. /// public static int winMajor => _winmajor; /// /// Gets Windows minor version. /// public static int winMinor => _winminor; /// /// Gets Windows build number. /// For example 14393 for Windows 10 version 1607. /// public static int winBuild => _winbuild; /// /// Gets Windows major and minor version in single int: Win7 - 0x601; Win8 - 0x602; Win8.1 - 0x603; Win10/11 - 0xA00. /// Example: if (osVersion.winVer >= osVersion.win8) ... /// public static int winVer => _winver; /// /// Windows version major+minor value that can be used with . /// Example: if (osVersion.winVer >= osVersion.win8) ... /// public const int win7 = 0x601, win8 = 0x602, win8_1 = 0x603, win10 = 0xA00; /// /// true if Windows 8.0 or later. /// public static bool minWin8 => _minWin8; /// /// true if Windows 8.1 or later. /// public static bool minWin8_1 => _minWin8_1; /// /// true if Windows 10 or later. /// public static bool minWin10 => _minWin10; /// /// true if Windows 10 version 1607 or later. /// public static bool minWin10_1607 => _win10build >= 14393; /// /// true if Windows 10 version 1703 or later. /// public static bool minWin10_1703 => _win10build >= 15063; /// /// true if Windows 10 version 1709 or later. /// public static bool minWin10_1709 => _win10build >= 16299; /// /// true if Windows 10 version 1803 or later. /// public static bool minWin10_1803 => _win10build >= 17134; /// /// true if Windows 10 version 1809 or later. /// public static bool minWin10_1809 => _win10build >= 17763; /// /// true if Windows 10 version 1903 or later. /// public static bool minWin10_1903 => _win10build >= 18362; /// /// true if Windows 10 version 1909 or later. /// public static bool minWin10_1909 => _win10build >= 18363; /// /// true if Windows 10 version 2004 or later. /// public static bool minWin10_2004 => _win10build >= 19041; /// /// true if Windows 10 version 20H2 or later. /// public static bool minWin10_20H2 => _win10build >= 19042; /// /// true if Windows 10 version 21H1 or later. /// public static bool minWin10_21H1 => _win10build >= 19043; /// /// true if Windows 10 version 21H2 or later. /// public static bool minWin10_21H2 => _win10build >= 19044; /// /// true if Windows 10 version 22H2 or later. /// public static bool minWin10_22H2 => _win10build >= 19045; /// /// true if Windows 11 or later. /// public static bool minWin11 => _win10build >= 22000; /// /// true if Windows 11 version 22H2 or later. /// public static bool minWin11_22H2 => _win10build >= 22621; /// /// true if Windows 11 version 23H2 or later. /// public static bool minWin11_23H2 => _win10build >= 22631; /// /// true if Windows 11 version 24H2 or later. /// public static bool minWin11_24H2 => _win10build >= 26100; /// /// true if Windows 11 version 25H2 or later. /// public static bool minWin11_25H2 => _win10build >= 26200; /// /// true if this process is ARM64. /// /// RuntimeInformation.ProcessArchitecture == Architecture.Arm64 public static bool isArm64Process => RuntimeInformation.ProcessArchitecture == Architecture.Arm64; /// /// true if Windows ARM64. /// /// RuntimeInformation.OSArchitecture == Architecture.Arm64 public static bool isArm64OS => RuntimeInformation.OSArchitecture == Architecture.Arm64; /// /// true if this process is 32-bit, false if 64-bit (x64 or ARM64). /// The same as sizeof(nint) == 4. /// public static bool is32BitProcess => sizeof(nint) == 4; /// /// true if Windows 32-bit, false if 64-bit (x64 or ARM64). /// public static bool is32BitOS => _is32BitOS; /// /// Returns true if this process is a 32-bit process running on 64-bit Windows. Also known as WOW64 process. /// public static bool is32BitProcessAnd64BitOS => _isWow64; /// /// Gets the version string of this library (Au.dll), like "1.2.3". /// /// /// This function is fast, unlike typeof(osVersion).Assembly.GetName().Version.ToString(3). /// /// LibreAutomate uses Au.dll of the same version as of LibreAutomate. /// public static string thisLibrary => Au_.Version; /// /// Gets string containing OS version, .NET version and Au.dll version, like "10.0.22621-64|6.0.8|1.2.3". /// Can be used for example to rebuild various caches when it's changed. /// public static string onaString => _environment.Value; static readonly Lazy _environment = new(() => $"{_winmajor.ToS()}.{_winminor.ToS()}.{_winbuild.ToS()}-{(_is32BitOS ? "32" : "64")}|{Environment.Version}|{Au_.Version}"); } ================================================ FILE: Au/System/process.cs ================================================ //#define USE_WTS //FUTURE: GetCpuUsage. namespace Au { /// /// Process functions. Find, enumerate, get basic info, terminate, triggers, etc. Also includes properties and events of current process and thread. /// /// /// /// public static unsafe class process { /// /// Gets process executable file name (like "notepad.exe") or full path. /// /// null if failed. /// Process id. /// /// Get full path. /// Note: Fails to get full path if the process belongs to another user session, unless current process is running as administrator; also fails to get full path of some system processes. /// /// When the fast API QueryFullProcessImageName fails, don't try to use another much slower API WTSEnumerateProcesses. Not used if fullPath is true. /// /// This function is much slower than getting window name or class name. /// /// /// /// public static string getName(int processId, bool fullPath = false, bool noSlowAPI = false) { if (processId == 0) return null; string R = null; //var t = perf.mcs; //if(s_time != 0) print.it(t - s_time); //s_time = t; using var ph = Handle_.OpenProcess(processId); if (!ph.Is0) { //In non-admin process fails if the process is of another user session. //Also fails for some system processes: nvvsvc, nvxdsync, dwm. For dwm fails even in admin process. //getting native path is faster, but it gets like "\Device\HarddiskVolume5\Windows\System32\notepad.exe" and I don't know API to convert to normal if (_QueryFullProcessImageName(ph, !fullPath, out var s)) { R = s; if (pathname.IsPossiblyDos_(R)) { if (fullPath || _QueryFullProcessImageName(ph, false, out s)) { R = pathname.ExpandDosPath_(s); if (!fullPath) R = _GetFileName(R); } } } } else if (!noSlowAPI && !fullPath) { //the slow way. Can get only names, not paths. using (var p = new AllProcesses_(false)) { for (int i = 0; i < p.Count; i++) if (p.Id(i) == processId) { R = p.Name(i, cannotOpen: true); break; } } //TEST: NtQueryInformationProcess, like in getCommandLine. } return R; //Would be good to cache process names here. But process id can be reused quickly. Use GetNameCached_ instead. // tested: a process id is reused after creating ~100 processes (and waiting until exits). It takes ~2 s. // The window finder is optimized to call this once for each process and not for each window. } /// /// Same as GetName, but faster when called several times for same window, like if(w.ProgramName=="A" || w.ProgramName=="B"). /// internal static string GetNameCached_(wnd w, int processId, bool fullPath = false) { if (processId == 0) return null; var cache = _LastWndProps.OfThread; cache.Begin(w); var R = fullPath ? cache.ProgramPath : cache.ProgramName; if (R == null) { R = getName(processId, fullPath); if (fullPath) cache.ProgramPath = R; else cache.ProgramName = R; } return R; } class _LastWndProps { wnd _w; long _time; internal string ProgramName, ProgramPath; internal void Begin(wnd w) { var t = Api.GetTickCount64(); if (w != _w || t - _time > 300) { _w = w; ProgramName = ProgramPath = null; } _time = t; } [ThreadStatic] static _LastWndProps _ofThread; internal static _LastWndProps OfThread => _ofThread ??= new(); } [SkipLocalsInit] static bool _QueryFullProcessImageName(IntPtr hProcess, bool getFilename, out string s) { s = null; using FastBuffer b = new(); for (; ; b.More()) { int n = b.n; if (Api.QueryFullProcessImageName(hProcess, getFilename, b.p, ref n)) { s = getFilename ? _GetFileName(b.p, n) : new string(b.p, 0, n); return true; } if (lastError.code != Api.ERROR_INSUFFICIENT_BUFFER) return false; } } #if USE_WTS //simple, safe, but ~2 times slower struct _AllProcesses :IDisposable { ProcessInfo_* _p; public _AllProcesses(out ProcessInfo_* p, out int count) { if(WTSEnumerateProcessesW(default, 0, 1, out p, out count)) _p = p; else _p = null; } public void Dispose() { if(_p != null) WTSFreeMemory(_p); } [DllImport("wtsapi32.dll", SetLastError = true)] static extern bool WTSEnumerateProcessesW(IntPtr serverHandle, uint reserved, uint version, out ProcessInfo_* ppProcessInfo, out int pCount); [DllImport("wtsapi32.dll", SetLastError = false)] static extern void WTSFreeMemory(ProcessInfo_* memory); } #else //the .NET Process class uses this. But it creates about 0.4 MB of garbage (last time tested was 0.2 MB). internal unsafe struct AllProcesses_ : IDisposable { readonly _ProcessInfo* _p; readonly int _count; static int s_bufferSize = 500_000; public AllProcesses_(bool ofThisSession) { int sessionId = ofThisSession ? thisProcessSessionId : 0; Api.SYSTEM_PROCESS_INFORMATION* b = null; try { for (int na = s_bufferSize; ;) { MemoryUtil.FreeAlloc(ref b, na); int status = Api.NtQuerySystemInformation(5, b, na, out na); //print.it(na); //~300_000, Win10, year 2021 if (status == 0) { s_bufferSize = na + 100_000; break; } if (status != Api.STATUS_INFO_LENGTH_MISMATCH) throw new AuException(status); } int nProcesses = 0, nbNames = 0; for (var p = b; ; p = (Api.SYSTEM_PROCESS_INFORMATION*)((byte*)p + p->NextEntryOffset)) { if (!ofThisSession || p->SessionId == sessionId) { nProcesses++; nbNames += p->NameLength; //bytes, not chars } if (p->NextEntryOffset == 0) break; } _count = nProcesses; _p = (_ProcessInfo*)MemoryUtil.Alloc(nProcesses * sizeof(_ProcessInfo) + nbNames); _ProcessInfo* r = _p; char* names = (char*)(_p + nProcesses); for (var p = b; ; p = (Api.SYSTEM_PROCESS_INFORMATION*)((byte*)p + p->NextEntryOffset)) { if (!ofThisSession || p->SessionId == sessionId) { r->processID = (int)p->UniqueProcessId; r->sessionID = (int)p->SessionId; int len = p->NameLength / 2; r->nameLen = len; if (len > 0) { //copy name to _p memory because it's in the huge buffer that will be released in this func r->nameOffset = (int)(names - (char*)_p); MemoryUtil.Copy((char*)p->NamePtr, names, len * 2); names += len; } else r->nameOffset = 0; //Idle r++; } if (p->NextEntryOffset == 0) break; } } finally { MemoryUtil.Free(b); } } public void Dispose() { MemoryUtil.Free(_p); } public int Count => _count; //public ProcessInfo this[int i] => new(ProcessName(i), _p[i].processID, _p[i].sessionID); //rejected, could be used where shouldn't, making code slower etc public ProcessInfo Info(int i) => new(Name(i), _p[i].processID, _p[i].sessionID); public int Id(int i) => (uint)i < _count ? _p[i].processID : throw new IndexOutOfRangeException(); public int SessionId(int i) => (uint)i < _count ? _p[i].sessionID : throw new IndexOutOfRangeException(); public string Name(int i, bool cannotOpen = false) => (uint)i < _count ? _p[i].GetName(_p, cannotOpen) : throw new IndexOutOfRangeException(); struct _ProcessInfo { public int sessionID; public int processID; public int nameOffset; public int nameLen; public string GetName(void* p, bool cannotOpen) { if (nameOffset == 0) { if (processID == 0) return "Idle"; return null; } string R = new((char*)p + nameOffset, 0, nameLen); if (!cannotOpen && pathname.IsPossiblyDos_(R)) { using var ph = Handle_.OpenProcess(processID); if (!ph.Is0 && _QueryFullProcessImageName(ph, false, out var s)) { R = _GetFileName(pathname.ExpandDosPath_(s)); } } return R; } } } #endif /// /// Gets basic info of all processes: name, id, session id. /// /// Get processes only of this user session (skip services etc). /// Failed. Unlikely. public static ProcessInfo[] allProcesses(bool ofThisSession = false) { using (var p = new AllProcesses_(ofThisSession)) { var a = new ProcessInfo[p.Count]; for (int i = 0; i < a.Length; i++) a[i] = p.Info(i); return a; } } /// /// Gets process ids of all processes of the specified program. /// /// Array containing zero or more elements. /// /// Process executable file name, like "notepad.exe". /// String format: [wildcard expression](xref:wildcard_expression). /// /// /// processName is full path. /// If null, calls . /// Note: Fails to get full path if the process belongs to another user session, unless current process is running as administrator; also fails to get full path of some system processes. /// /// Get processes only of this user session. /// /// - processName is "" or null. /// - Invalid wildcard expression ("**options " or regular expression). /// public static int[] getProcessIds([ParamString(PSFormat.Wildex)] string processName, bool? fullPath = false, bool ofThisSession = false) { List a = null; bool fp = _NameOrPath(ref processName, fullPath); GetProcessesByName_(ref a, processName, fp, ofThisSession); return a?.ToArray() ?? []; } static bool _NameOrPath(ref string processName, bool? fullPath) { if (processName.NE()) throw new ArgumentException(); return fullPath switch { false => false, true => pathname.isFullPathExpand(ref processName, strict: false) | true, _ => pathname.isFullPathExpand(ref processName, strict: false) }; } /// /// Gets process id of the first found process of the specified program. /// /// 0 if not found. /// public static int getProcessId([ParamString(PSFormat.Wildex)] string processName, bool? fullPath = false, bool ofThisSession = false) { List a = null; bool fp = _NameOrPath(ref processName, fullPath); return GetProcessesByName_(ref a, processName, fp, ofThisSession, true); } /// /// Returns true if a process of the specified program is running. /// /// public static bool exists([ParamString(PSFormat.Wildex)] string processName, bool? fullPath = false, bool ofThisSession = false) => 0 != getProcessId(processName, fullPath, ofThisSession); internal static int GetProcessesByName_(ref List a, wildex processName, bool fullPath = false, bool ofThisSession = false, bool first = false) { a?.Clear(); using (var p = new AllProcesses_(ofThisSession)) { for (int i = 0; i < p.Count; i++) { string s = fullPath ? getName(p.Id(i), true) : p.Name(i); if (s != null && processName.Match(s)) { int pid = p.Id(i); if (first) return pid; a ??= new List(); a.Add(pid); } } } return 0; } static string _GetFileName(char* s, int len) { if (s == null) return null; char* ss = s + len; for (; ss > s; ss--) if (ss[-1] == '\\' || ss[-1] == '/') break; return new string(ss, 0, len - (int)(ss - s)); } static string _GetFileName(string s) { fixed (char* p = s) return _GetFileName(p, s.Length); } /// /// Gets version info of process executable file. /// /// null if failed. /// Process id. public static FileVersionInfo getVersionInfo(int processId) { var s = getName(processId, true); if (s != null) { try { return FileVersionInfo.GetVersionInfo(s); } catch { } } return null; } /// /// Gets description of process executable file. /// /// null if failed. /// Process id. /// /// Calls and . /// public static string getDescription(int processId) => getVersionInfo(processId)?.FileDescription; /// /// Gets process id from handle (API GetProcessId). /// /// 0 if failed. Supports . /// Process handle. public static int processIdFromHandle(IntPtr processHandle) => Api.GetProcessId(processHandle); //fast //public static Process processObjectFromHandle(IntPtr processHandle) //{ // int pid = GetProcessId(processHandle); // if(pid == 0) return null; // return Process.GetProcessById(pid); //slow, makes much garbage, at first gets all processes just to throw exception if pid not found... //} /// /// Gets user session id of a process (API ProcessIdToSessionId). /// /// Returns -1 if failed. Supports . /// Process id. public static int getSessionId(int processId) { if (!Api.ProcessIdToSessionId(processId, out var R)) return -1; return R; } /// /// Gets process creation and execution times (API GetProcessTimes). /// /// false if failed. Supports . /// Process id. /// Creation time. As absolute FILETIME, UTC. If you need , use . /// Amount of time spent executing code (using CPU). As FILETIME. If you need , use . public static bool getTimes(int processId, out long created, out long executed) { created = 0; executed = 0; using var ph = Handle_.OpenProcess(processId); if (ph.Is0 || !Api.GetProcessTimes(ph, out created, out _, out long tk, out long tu)) return false; executed = tk + tu; return true; } /// /// Returns true if the process is 32-bit, false if 64-bit. /// Also returns false if failed. Supports . /// /// /// If you know it is current process, instead use functions or IntPtr.Size==4. This function is much slower. /// /// public static bool is32Bit(int processId) { bool is32bit = osVersion.is32BitOS; if (!is32bit) { using var ph = Handle_.OpenProcess(processId); if (ph.Is0 || !Api.IsWow64Process(ph, out is32bit)) return false; } lastError.clear(); return is32bit; //info: don't use Process.GetProcessById, it does not have a desiredAccess parameter and fails with higher IL processes. } /// /// Returns true if the process is 32-bit, false if 64-bit. /// Also returns false if failed. Supports . /// public static bool is32Bit(IntPtr processHandle) { bool is32bit = osVersion.is32BitOS; if (!is32bit) { if (!Api.IsWow64Process(processHandle, out is32bit)) return false; } lastError.clear(); return is32bit; } //rejected: isArm64, or architecture, or cpuArch. Rarely used. /// /// Gets the command line string used to start the specified process. /// /// null if failed. /// Process id. /// Remove program path. Return only arguments, or empty string if there is no arguments. /// /// The string starts with program file path or name, often enclosed in "", and may be followed by arguments. Some processes may modify it; then this function gets the modified string. /// Fails if the specified process is admin and this process isn't. May fail with some system processes. Fails if this is a 32-bit process. /// public static unsafe string getCommandLine(int processId, bool removeProgram = false) { if (osVersion.is32BitProcess) return null; //can't get PEB address of 64-bit processes. Never mind 32-bit OS. using var pm = new ProcessMemory(processId, 0, noException: true); if (pm.ProcessHandle == default) return null; Api.PROCESS_BASIC_INFORMATION pbi = default; if (0 == Api.NtQueryInformationProcess(pm.ProcessHandle, 0, &pbi, sizeof(Api.PROCESS_BASIC_INFORMATION), out _)) { long upp; Api.RTL_USER_PROCESS_PARAMETERS up; if (pm.Read((IntPtr)pbi.PebBaseAddress + 32, &upp, 8) && pm.Read((IntPtr)upp, &up, sizeof(Api.RTL_USER_PROCESS_PARAMETERS))) { pm.Mem = (IntPtr)up.CommandLine.Buffer; var s = pm.ReadCharString(up.CommandLine.Length / 2) ?.Trim() //many end with space, usually when without commandline args ?.Replace('\0', ' '); //sometimes '\0' instead of spaces before args if (removeProgram) s = s.RxReplace(@"(?i)^(?:"".+?""|\S+)(?:\s+(.*))?", "$1"); return s; } } return null; //speed: ~25 mcs cold. WMI Win32_Process ~50 ms (2000 times slower). } internal static unsafe int GetParentProcessId_() { Api.PROCESS_BASIC_INFORMATION pbi = default; if (0 != Api.NtQueryInformationProcess(thisProcessHandle, 0, &pbi, sizeof(Api.PROCESS_BASIC_INFORMATION), out _)) return 0; return (int)pbi.ParentProcessId; } /// /// Terminates (ends) the specified process. /// /// false if failed. Supports . /// Process id. /// Process exit code. /// /// Uses API WTSTerminateProcess or TerminateProcess. They are async; usually the process ends after 2 - 200 ms, depending on program etc. /// /// Does not try to end process "softly" (close main window). Unsaved data will be lost. /// /// Alternatives: run taskkill.exe or pskill.exe (download). See . More info on the internet. /// /// /// Restart the shell process (explorer). /// /// public static bool terminate(int processId, int exitCode = 0) { if (Api.WTSTerminateProcess(default, processId, exitCode)) return true; if (lastError.code != Api.ERROR_INVALID_PARAMETER) { using var h = Handle_.OpenProcess(processId, Api.SYNCHRONIZE | Api.PROCESS_TERMINATE); if (!h.Is0) { if (!Api.TerminateProcess(h, exitCode)) return 0 == Api.WaitForSingleObject(h, 500); //ERROR_ACCESS_DENIED when the process is ending return true; } } return false; //note: TerminateProcess and WTSTerminateProcess are async. Tested programs ended after 3 - 150 ms, depending on program. } /// /// Terminates (ends) all processes of the specified program or programs. /// /// The number of successfully terminated processes. /// /// Process executable file name (like "notepad.exe") or full path. /// String format: [wildcard expression](xref:wildcard_expression). /// /// Processes of any user session. If false (default), only processes of this user session. /// Process exit code. /// /// - processName is "" or null. /// - Invalid wildcard expression ("**options " or regular expression). /// public static int terminate(string processName, bool allSessions = false, int exitCode = 0) { int n = 0; foreach (int pid in getProcessIds(processName, fullPath: null, ofThisSession: !allSessions)) { if (terminate(pid, exitCode)) n++; } return n; } /// /// Suspends or resumes the specified process. /// /// false if failed. Supports . /// true suspend, false resume. /// Process id. /// /// If suspended multiple times, must be resumed the same number of times. /// public static bool suspend(bool suspend, int processId) { using var hp = Handle_.OpenProcess(processId, Api.PROCESS_SUSPEND_RESUME); if (!hp.Is0) { int status = suspend ? Api.NtSuspendProcess(hp) : Api.NtResumeProcess(hp); lastError.code = status; return status == 0; } return false; } /// /// Suspends or resumes all processes of the specified program or programs. /// /// The number of successfully suspended/resumed processes. /// true suspend, false resume. /// /// Process executable file name (like "notepad.exe") or full path. /// String format: [wildcard expression](xref:wildcard_expression). /// /// Processes of any user session. If false (default), only processes of this user session. /// /// - processName is "" or null. /// - Invalid wildcard expression ("**options " or regular expression). /// /// /// If suspended multiple times, must be resumed the same number of times. /// public static int suspend(bool suspend, string processName, bool allSessions = false) { int n = 0; foreach (int pid in getProcessIds(processName, fullPath: null, ofThisSession: !allSessions)) { if (process.suspend(suspend, pid)) n++; } return n; } /// /// Waits until the process ends. /// /// true when the process ended. On timeout returns false if timeout is negative; else exception. /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// Process id. If invalid but not 0, the function returns true and sets exitCode = int.MinValue; probably the process is already ended. /// Receives the exit code. /// timeout time has expired (if > 0). /// Failed. /// processId is 0. public static bool waitForExit(Seconds timeout, int processId, out int exitCode) { if (processId == 0) throw new ArgumentException("processId 0", nameof(processId)); using var h = Handle_.OpenProcess(processId, Api.SYNCHRONIZE | Api.PROCESS_QUERY_LIMITED_INFORMATION); if (h.Is0) { var e = lastError.code; if (e == Api.ERROR_INVALID_PARAMETER) { exitCode = int.MinValue; return true; }; throw new AuException(e); } exitCode = 0; if (0 == wait.forHandle(timeout, 0, h)) return false; Api.GetExitCodeProcess(h, out exitCode); return true; } /// /// Provides process started/ended triggers in foreach loop. See examples. /// /// /// An object that retrieves process trigger info (started/ended, name, id, session id) when used with foreach. /// If need more process properties, your code can call class functions with the process id. /// /// Trigger events: true - started, false - ended, null (default) - both. /// /// Process executable file name, like "notepad.exe". /// String format: [wildcard expression](xref:wildcard_expression). /// null matches all. /// /// Watch processes only of this user session. /// /// The period in milliseconds of retrieving the list of processes for detecting new and ended processes. Default 100, min 10, max 1000. /// Smaller = smaller average delay and less missing triggers (when process lifetime is very short) but more CPU usage. /// /// Invalid wildcard expression ("**options " or regular expression). /// /// true
, "notepad.exe", ofThisSession: true)) { /// print.it(v); /// } /// ]]> /// public static IEnumerable triggers(bool? started = null, [ParamString(PSFormat.Wildex)] string processName = null, bool ofThisSession = false, int period = 100) { wildex wild = processName; period = Math.Clamp(period, 10, 1000) - 2; var comparer = new _PiComparer(); var hs = new HashSet(comparer); var ap = allProcesses(ofThisSession); for (; ; ) { period.ms(); //Debug_.MemorySetAnchor_(); //perf.first(); using (var p = new AllProcesses_(ofThisSession)) { //perf.next(); bool eq = p.Count == ap.Length; if (eq) for (int i = 0; i < ap.Length; i++) if (!(eq = ap[i].Id == p.Id(i))) break; if (!eq) { var a = new ProcessInfo[p.Count]; for (int i = 0; i < a.Length; i++) a[i] = p.Info(i); for (int i = 0; i < 2; i++) { ProcessInfo[] a1, a2; if (i == 0) { if (started == true) continue; a1 = a; a2 = ap; } else { if (started == false) continue; a1 = ap; a2 = a; } hs.Clear(); hs.UnionWith(a1); foreach (var v in a2) { if (hs.Add(v)) { if (wild == null || wild.Match(v.Name)) yield return new(i == 1, v.Name, v.Id, v.SessionId); } } } ap = a; } } //perf.nw(); //Debug_.MemoryPrint_(); } } class _PiComparer : IEqualityComparer { //public bool Equals(ProcessInfo x, ProcessInfo y) => x.Id == y.Id; public bool Equals(ProcessInfo x, ProcessInfo y) => x.Id == y.Id && x.Name == y.Name; public int GetHashCode(ProcessInfo obj) => obj.Id; } #region this process /// /// Gets current process id. /// See API GetCurrentProcessId. /// public static int thisProcessId => Api.GetCurrentProcessId(); /// /// Returns current process handle. /// See API GetCurrentProcess. /// Don't need to close the handle. /// public static IntPtr thisProcessHandle => Api.GetCurrentProcess(); //rejected. Too simple and rare. ///// ///// Gets native module handle of the program file of this process. ///// //public static IntPtr thisExeModuleHandle => Api.GetModuleHandle(null); /// /// Gets full path of the program file of this process. /// [SkipLocalsInit] public static unsafe string thisExePath => Environment.ProcessPath; /// /// Gets file name of the program file of this process, like "name.exe". /// public static string thisExeName => s_exeName ??= pathname.getName(thisExePath); static string s_exeName; /// /// Gets user session id of this process. /// public static int thisProcessSessionId => getSessionId(Api.GetCurrentProcessId()); /// /// Gets or sets whether and are . /// /// /// If your app doesn't want to use current culture (default in .NET apps), it can set these properties = or set this property = true. /// It prevents potential bugs when app/script/components don't specify invariant culture in string functions and "number to/from string" functions. /// Also, there is a bug in "number to/from string" functions in some .NET versions with some cultures: they use wrong minus sign, not ASCII '-' which is specified in Control Panel. /// The default compiler sets this property = true; as well as . /// public static bool thisProcessCultureIsInvariant { get { var ic = CultureInfo.InvariantCulture; return CultureInfo.DefaultThreadCurrentCulture == ic && CultureInfo.DefaultThreadCurrentUICulture == ic; } set { if (value) { var ic = CultureInfo.InvariantCulture; CultureInfo.DefaultThreadCurrentCulture = ic; CultureInfo.DefaultThreadCurrentUICulture = ic; } else { CultureInfo.DefaultThreadCurrentCulture = null; CultureInfo.DefaultThreadCurrentUICulture = null; } } } /// /// true in LA main process. LA sets it, except in tools/pip/dragdrop processes. /// internal static bool IsLaProcess_; /// /// true in main thread of main LA process. LA sets it, except in tools/pip/dragdrop processes. This is a [ThreadStatic] variable. /// NOTE: don't use Environment.CurrentManagedThreadId == 1, it's not always 1 in the main thread. /// [ThreadStatic] internal static bool IsLaMainThread_; /// /// After afterMS milliseconds invokes GC and calls API SetProcessWorkingSetSize. /// internal static void ThisProcessMinimizePhysicalMemory_(int afterMS) { Task.Delay(afterMS).ContinueWith(_ => { GC.Collect(); GC.WaitForPendingFinalizers(); Api.SetProcessWorkingSetSize(Api.GetCurrentProcess(), -1, -1); }); } //internal static (long WorkingSet, long PageFile) ThisProcessGetMemoryInfo_() //{ // Api.PROCESS_MEMORY_COUNTERS m = default; m.cb = sizeof(Api.PROCESS_MEMORY_COUNTERS); // Api.GetProcessMemoryInfo(ProcessHandle, ref m, m.cb); // return ((long)m.WorkingSetSize, (long)m.PagefileUsage); //} /// /// Before this process exits, either normally or on unhandled exception. /// /// /// The event handler is called on: ///
, with parameter = null. ///
, with parameter = the . ///
public static event Action thisProcessExit { add { if (!_haveEventExit) { lock ("AVCyoRcQCkSl+3W8ZTi5oA") { if (!_haveEventExit) { var d = AppDomain.CurrentDomain; d.ProcessExit += _ThisProcessExit; d.UnhandledException += _ThisProcessExit; //because ProcessExit is missing on exception _haveEventExit = true; } } } _eventExit += value; } remove { _eventExit -= value; } } static Action _eventExit; static bool _haveEventExit; static void _ThisProcessExit(object sender, EventArgs ea) { //sender: AppDomain on process exit, null on unhandled exception Exception e; if (ea is UnhandledExceptionEventArgs u) { if (!u.IsTerminating) return; //never seen, but anyway e = (Exception)u.ExceptionObject; //probably non-Exception object is impossible in C# } else { e = script.s_unhandledException; } var k = _eventExit; if (k != null) { try { _eventExit = null; k(e); } catch (Exception e1) { print.qm2.writeD("_ThisProcessExit", e1); } } thisProcessExitDone_?.Invoke(); } internal static event Action thisProcessExitDone_; /// /// Calls and removes all event handlers. /// /// /// Call this if event handlers don't run because this process is terminated before it. For example when current session is ending (shutdown, restart, logoff); to detect it can be used Application.SessionEnding, Application.OnSessionEnding or WM_QUERYENDSESSION. /// public static void thisProcessExitInvoke() { var k = _eventExit; if (k != null) try { _eventExit = null; k(null); } catch { } } #endregion #region this thread /// /// Gets native thread id of this thread (API GetCurrentThreadId). /// /// /// It is not the same as . /// /// public static int thisThreadId => Api.GetCurrentThreadId(); //speed: fast, but several times slower than Environment.CurrentManagedThreadId. Caching in a ThreadStatic variable makes even slower. /// /// Returns native thread handle of this thread (API GetCurrentThread). /// public static IntPtr thisThreadHandle => Api.GetCurrentThread(); /// /// Returns true if this thread has a .NET message loop (winforms or WPF). /// /// Has WPF message loop and no winforms message loop. /// public static bool thisThreadHasMessageLoop(out bool isWPF) { //info: we don't call .NET functions directly to avoid loading assemblies. isWPF = false; int f = AssemblyUtil_.IsLoadedWinformsWpf(); if (0 != (f & 1) && _HML_Forms()) return true; if (0 != (f & 2) && _HML_Wpf()) return isWPF = true; return false; } /// public static bool thisThreadHasMessageLoop() => thisThreadHasMessageLoop(out _); [MethodImpl(MethodImplOptions.NoInlining)] static bool _HML_Forms() => System.Windows.Forms.Application.MessageLoop; [MethodImpl(MethodImplOptions.NoInlining)] static bool _HML_Wpf() { if (SynchronizationContext.Current is System.Windows.Threading.DispatcherSynchronizationContext) { var d = System.Windows.Threading.Dispatcher.FromThread(Thread.CurrentThread); if (d != null) { var f = typeof(System.Windows.Threading.Dispatcher).GetField("_frameDepth", BindingFlags.Instance | BindingFlags.NonPublic); Debug_.PrintIf(f == null); return f == null || f.GetValue(d) is not int i || i > 0; } } return false; } //static bool _HML_Wpf() => System.Windows.Threading.Dispatcher.FromThread(Thread.CurrentThread) != null; //no. Not null after a loop ends or even after XamlReader.Parse. //static bool _HML_Wpf() => SynchronizationContext.Current is System.Windows.Threading.DispatcherSynchronizationContext; //no. True eg in Dispatcher.Invoke callback. [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static void ThisThreadSetComApartment_(ApartmentState state) { var t = Thread.CurrentThread; t.TrySetApartmentState(ApartmentState.Unknown); t.TrySetApartmentState(state); //CONSIDER: use OleInitialize instead of t.TrySetApartmentState(state). // Somehow RegisterDragDrop in UacDragDrop fails if ThisThreadSetComApartment_. // But RDD in SciCode works with this. //This is undocumented, but works if we set ApartmentState.Unknown at first. //With [STAThread] slower, and the process initially used to have +2 threads. //Speed when called to set STA at startup: 1.7 ms. If apphost calls OleInitialize, 1.5 ms. //tested: OleUninitialize in apphost does not make GetApartmentState return MTA. } #endregion } } namespace Au.Types { /// /// Contains process name (like "notepad.exe"), id and user session id. /// public record struct ProcessInfo(string Name, int Id, int SessionId); //use record to auto-implement ==, eg for code like var a=process.allProcesses(); 5.s(); print.it(process.allProcesses().Except(a)); /// /// Contains process trigger info retrieved by . /// public record class ProcessTriggerInfo(bool Started, string Name, int Id, int SessionId); } ================================================ FILE: Au/System/run.cs ================================================ using Microsoft.Win32.SafeHandles; namespace Au { /// /// Execute or open programs, files, folders, web pages, etc, start new threads. /// public static class run { /// /// Runs/opens a program, document, directory (folder), URL, new email, etc. /// /// Process info (id etc). /// /// Examples: ///
@"C:\file.txt" ///
folders.Documents ///
folders.System + "notepad.exe" ///
@"%folders.System%\notepad.exe" ///
@"%TMP%\file.txt" ///
"notepad.exe" ///
@"..\folder\x.exe" ///
"http://a.b.c/d" ///
"file:///path" ///
"mailto:a@b.c" ///
":: ITEMIDLIST" ///
@"shell:::{CLSID}" ///
@"shell:AppsFolder\Microsoft.WindowsCalculator_8wekyb3d8bbwe!App". /// /// /// Command line arguments. /// This function expands environment variables if starts with "%" or "\"%". /// /// /// /// Allows to specify more parameters: current directory, verb, etc. /// If string, it sets initial current directory for the new process. If "", gets it from file. More info: . /// /// Used both and and this process isn't admin. /// Failed. For example, the file does not exist. /// /// It works like when you double-click a file icon. It may start new process or not. For example it may just activate window if the program is already running. /// Uses API ShellExecuteEx. /// Similar to . /// /// The file parameter can be: /// - Full path of a file or directory. Examples: @"C:\file.txt", folders.Documents, folders.System + "notepad.exe", @"%folders.System%\notepad.exe". /// - Filename of a file or directory, like "notepad.exe". The function calls . /// - Path relative to . Examples: "x.exe", @"subfolder\x.exe", @".\subfolder\x.exe", @"..\another folder\x.exe". /// - URL. Examples: "https://www.example.com", "file:///path". /// - Email, like "mailto:a@b.c". Subject, body etc also can be specified, and Google knows how. /// - Shell object's ITEMIDLIST like ":: ITEMIDLIST". See , . Can be used to open virtual folders and items like Control Panel. /// - Shell object's parsing name, like @"shell:::{CLSID}" or @"::{CLSID}". See . Can be used to open virtual folders and items like Control Panel. /// - To run a Windows Store App, use @"shell:AppsFolder\WinStoreAppId" format. Example: @"shell:AppsFolder\Microsoft.WindowsCalculator_8wekyb3d8bbwe!App". To discover the string use hotkey Ctrl+Shift+Q or function or Google. /// - To open a Windows Settings page can be used ms-settings, like "ms-settings:display". To open Settings use "ms-settings:". /// /// Supports environment variables, like @"%TMP%\file.txt". See . /// /// By default the new process isn't admin even if this process is admin. It's a unique feature of this function. More info: . /// /// The new process inherits environment variables of this process only if both processes are admin or non-admin. To ensure it, use flag or some other "start process" function (, , Windows API). /// /// /// /// /// /// Run Notepad and wait for an active Notepad window. /// /// Run Notepad or activate a Notepad window. /// run.it("notepad.exe")); /// ]]> /// Run File Explorer and wait for new folder window. Ignores matching windows that already existed. /// run.it(@"explorer.exe"), /// 10, cn: "CabinetWClass"); /// ]]> /// public static RResult it(string file, string args = null, RFlags flags = 0, ROptions dirEtc = null) { Api.SHELLEXECUTEINFO x = default; x.cbSize = Api.SizeOf(x); x.fMask = Api.SEE_MASK_NOZONECHECKS | Api.SEE_MASK_NOASYNC | Api.SEE_MASK_CONNECTNETDRV | Api.SEE_MASK_UNICODE; x.nShow = Api.SW_SHOWNORMAL; bool curDirFromFile = false; var more = dirEtc; if (more != null) { x.lpVerb = more.Verb; if (x.lpVerb != null) x.fMask |= Api.SEE_MASK_INVOKEIDLIST; //makes slower. But verbs are rarely used. if (more.CurrentDirectory is string cd) { if (cd.Length == 0) curDirFromFile = true; else cd = pathname.expand(cd); x.lpDirectory = cd; } if (!more.OwnerWindow.IsEmpty) x.hwnd = more.OwnerWindow.Hwnd.Window; switch (more.WindowState) { case ProcessWindowStyle.Hidden: x.nShow = Api.SW_HIDE; break; case ProcessWindowStyle.Minimized: x.nShow = Api.SW_SHOWMINIMIZED; break; case ProcessWindowStyle.Maximized: x.nShow = Api.SW_SHOWMAXIMIZED; break; } x.fMask &= ~more.FlagsRemove; x.fMask |= more.FlagsAdd; } if (flags.Has(RFlags.Admin)) { if (x.lpVerb == null || x.lpVerb.Eqi("runas")) x.lpVerb = "runas"; else if (!uacInfo.isAdmin) throw new ArgumentException("Cannot use Verb with flag Admin, unless this process is admin"); } file = NormalizeFile_(false, file, out bool isFullPath, out bool isShellPath); Pidl pidl = null; if (isShellPath) { //":: ITEMIDLIST" or "::{CLSID}..." (we convert it too because the API does not support many) pidl = Pidl.FromString(file); //does not throw if (pidl != null) { x.lpIDList = pidl.UnsafePtr; x.fMask |= Api.SEE_MASK_INVOKEIDLIST; } else x.lpFile = file; } else { x.lpFile = file; if (curDirFromFile && isFullPath) x.lpDirectory = pathname.getDirectory(file); } x.lpDirectory ??= Directory.GetCurrentDirectory(); if (!args.NE()) x.lpParameters = pathname.expand(args); if (0 == (flags & RFlags.ShowErrorUI)) x.fMask |= Api.SEE_MASK_FLAG_NO_UI; if (0 == (flags & RFlags.WaitForExit)) x.fMask |= Api.SEE_MASK_NO_CONSOLE; if (0 != (flags & RFlags.MostUsed)) x.fMask |= Api.SEE_MASK_FLAG_LOG_USAGE; x.fMask |= Api.SEE_MASK_NOCLOSEPROCESS; WndUtil.EnableActivate(-1); bool waitForExit = 0 != (flags & RFlags.WaitForExit); bool needHandle = flags.Has(RFlags.NeedProcessHandle); bool ok = false; int pid = 0, errorCode = 0; bool asUser = !flags.HasAny(RFlags.Admin | RFlags.InheritAdmin) && uacInfo.ofThisProcess.Elevation == UacElevation.Full; //info: new process does not inherit uiAccess if (asUser) { ok = Cpp.Cpp_ShellExec(x, out pid, out int injectError, out int execError); if (!ok) { if (injectError != 0) { print.warning("Failed to run as non-admin."); //once in TT process started to always fail. More info in UnmarshalAgentIAccessible(). asUser = false; } else errorCode = execError; } } if (!asUser) { ok = Api.ShellExecuteEx(ref x); if (!ok) errorCode = lastError.code; } pidl?.Dispose(); if (!ok) throw new AuException(errorCode, $"*run '{file}'"); var R = new RResult(); WaitHandle_ ph = null; if (needHandle || waitForExit) { if (pid != 0) x.hProcess = Handle_.OpenProcess(pid, Api.PROCESS_ALL_ACCESS); if (!x.hProcess.Is0) ph = new WaitHandle_(x.hProcess, true); } if (!waitForExit) { if (pid != 0) R.ProcessId = pid; else if (!x.hProcess.Is0) R.ProcessId = process.processIdFromHandle(x.hProcess); } try { Api.AllowSetForegroundWindow(); if (x.lpVerb != null && Thread.CurrentThread.GetApartmentState() == ApartmentState.STA) Thread.CurrentThread.Join(50); //need min 5-10 for file Properties. And not Sleep. if (ph != null) { if (waitForExit) { ph.WaitOne(); if (Api.GetExitCodeProcess(x.hProcess, out var exitCode)) R.ProcessExitCode = exitCode; } if (needHandle) R.ProcessHandle = ph; } } finally { if (R.ProcessHandle == null) { if (ph != null) ph.Dispose(); else x.hProcess.Dispose(); } } return R; //tested: works well in MTA thread. //rejected: in QM2, run also has a 'window' parameter. However it just makes limited, unclear etc, and therefore rarely used. Instead use wnd.findOrRun etc like in the examples. //rejected: in QM2, run also has 'autodelay'. Better don't add such hidden things. Let the script decide what to do. } /// /// Calls and handles exceptions. /// If throws exception, writes it to the output as warning and returns null. /// /// /// This function is useful when you don't care whether succeeded and don't want to use try/catch. /// Handles only exception of type . It is thrown when fails, usually when the file does not exist. /// /// /// /// /// [MethodImpl(MethodImplOptions.NoInlining)] //uses stack public static RResult itSafe(string file, string args = null, RFlags flags = 0, ROptions dirEtc = null) { try { return it(file, args, flags, dirEtc); } catch (AuException e) { print.warning(e); return null; } } internal static string NormalizeFile_(bool runConsole, string file, out bool isFullPath, out bool isShellPath) { isShellPath = isFullPath = false; file = pathname.expand(file); if (file.NE()) throw new ArgumentException(); if (runConsole || !(isShellPath = pathname.IsShellPath_(file))) { if (isFullPath = pathname.isFullPath(file)) { var fl = runConsole ? PNFlags.DontExpandDosPath : PNFlags.DontExpandDosPath | PNFlags.DontPrefixLongPath; file = pathname.Normalize_(file, fl, true); //ShellExecuteEx supports long path prefix for exe but not for documents. //Process.Start supports long path prefix, except when the exe is .NET. if (!runConsole) file = pathname.unprefixLongPath(file); if (FileSystemRedirection.IsSystem64PathIn32BitProcess(file) && !filesystem.exists(file)) { file = FileSystemRedirection.GetNonRedirectedSystemPath(file); } } else if (!pathname.isUrl(file)) { //ShellExecuteEx searches everywhere except in app folder. //Process.Start prefers current directory. var s2 = filesystem.searchPath(file); if (s2 != null) { file = s2; isFullPath = true; } } } return file; } /// /// Runs a console program in hidden mode, waits until its process ends, and prints its output text. /// Writes text lines to the output in real time. /// /// /// Path or name of an .exe or .bat file. Can be: ///
• Full path. Examples: @"C:\folder\x.exe", folders.System + "x.exe", @"%folders.System%\x.exe". ///
• Filename, like "x.exe". This function calls . ///
• Path relative to . Examples: "x.exe", @"subfolder\x.exe", @".\subfolder\x.exe", @"..\folder\x.exe". /// ///
Supports environment variables, like @"%TMP%\x.bat". See . /// /// null or command line arguments. /// /// Initial current directory of the new process. ///
• If null, uses Directory.GetCurrentDirectory(). ///
• Else if "", calls pathname.getDirectory(exe). ///
• Else calls . /// /// /// Console's text encoding. /// If null (default), uses . If you get garbage text, try or . /// /// The process exit code. Usually a non-0 value means error. /// Failed, for example file not found. /// /// The console window is hidden. The text that would be displayed in it is redirected to this function. /// /// Console programs have two output text streams - standard output and standard error. This function gets both. Alternatively use ; it gets the output and error streams separately, and some lines may be received in incorrect order in time. /// /// /// /// public static int console(string exe, string args = null, string curDir = null, Encoding encoding = null) { return _RunConsole(print.it, out _, exe, args, curDir, encoding, true); } /// /// Runs a console program in hidden mode, waits until its process ends, and gets its output text. /// /// A variable that receives the output text. /// /// /// /// public static int console(out string output, string exe, string args = null, string curDir = null, Encoding encoding = null) { var r = _RunConsole(null, out output, exe, args, curDir, encoding, false); return r; } /// /// Runs a console program in hidden mode, waits until its process ends, and gets its output text. /// Uses a callback function that receives text lines in real time. /// /// /// Callback function that receives the output text. /// Unless rawText true: ///
• it isn't called until is retrieved full line with line break characters; ///
• it receives single full line at a time, without line break characters. /// /// Call the callback function whenever text is retrieved (don't wait for full line). Pass raw text, in chunks of any size. /// /// print.it(s), @"C:\Test\console.exe"); /// /// run.console(s => { print.it($"<><_>{s}"); }, @"C:\Test\console.exe", rawText: true); /// ]]> /// /// /// public static int console(Action output, string exe, string args = null, string curDir = null, Encoding encoding = null, bool rawText = false) { return _RunConsole(output, out _, exe, args, curDir, encoding, !rawText); } static unsafe int _RunConsole(Action outAction, out string outStr, string exe, string args, string curDir, Encoding encoding, bool needLines) { outStr = null; using var c = new consoleProcess(exe, args, curDir) { Encoding = encoding }; if (needLines) { while (c.ReadLine(out var s)) outAction(s); } else if (outAction != null) { c.ReadAllText(outAction); } else { outStr = c.ReadAllText(); } return c.ExitCode; } /// /// Opens parent folder in File Explorer (folder window) and selects the file. /// /// false if failed, for example if the file does not exist. /// /// Full path of a file or directory or other shell object. /// Supports @"%environmentVariable%\..." (see ) and "::..." (see ). /// public static bool selectInExplorer(string path) { using var pidl = Pidl.FromString(path); if (pidl == null) return false; return 0 == Api.SHOpenFolderAndSelectItems(pidl.HandleRef, 0, null, 0); } /// /// Starts new thread: creates new object, sets some properties and calls . /// /// The Thread variable. /// Thread procedure. Parameter start of constructor. /// /// If true (default), sets = true. /// The process ends when the main thread and all foreground threads end; background threads then are terminated. /// /// If true (default), sets . /// public static Thread thread(Action threadProc, bool background = true, bool sta = true) { var t = new Thread(threadProc.Invoke); if (background) t.IsBackground = true; if (sta) t.SetApartmentState(ApartmentState.STA); t.Start(); return t; } /// /// Starts new thread like and gets thread handle and native id. /// /// Native thread id. /// Thread object. /// Called in the new thread before threadProc. This function (run.thread) waits until it returns. /// Thread handle. Don't forget to dispose. /// public static unsafe SafeWaitHandle thread(out int id, out Thread thread, Action threadProc, bool background = true, bool sta = true, Action init = null) { SafeWaitHandle h = null; int i = 0; using var ev = Api.CreateEvent(false); thread = new Thread(() => { init?.Invoke(); h = new(Api.OpenThread(Api.THREAD_ALL_ACCESS, false, i = Api.GetCurrentThreadId()), ownsHandle: true); Api.SetEvent(ev); threadProc(); }); if (background) thread.IsBackground = true; if (sta) thread.SetApartmentState(ApartmentState.STA); thread.Start(); Api.WaitForSingleObject(ev, -1); id = i; return h; //Almost same speed as other overload when JITed, but first time several times slower, eg 1 -> 2.5 ms. // With CreateThread faster, but it cannot be used in a public function (then some .NET features work differently). } } } namespace Au.Types { /// /// Flags for . /// [Flags] public enum RFlags { /// /// Show error message box if fails, for example if file not found. /// Note: this does not disable exceptions. To avoid exceptions use try/catch or . /// ShowErrorUI = 1, /// /// If started new process, wait until it exits. /// WaitForExit = 2, /// /// If started new process, get process handle (). /// NeedProcessHandle = 4, /// /// Run new process as administrator. ///
If this process isn't admin: ///
• Shows UAC consent dialog. ///
• Uses verb "runas", therefore other verb cannot be specified. ///
• Cannot set current directory for the new process. ///
• The new process does not inherit environment variables of this process. ///
Admin = 8, /// /// If this process runs as administrator, run new process as administrator too. ///
Without this flag, if this process runs as administrator: ///
• Starts new process as non-administrator from the shell process (explorer.exe). ///
• If it fails (for example if shell process isn't running), calls and starts new process as administrator. ///
• The new process does not inherit environment variables of this process. ///
InheritAdmin = 16, /// /// Add the app to the "Most used" list in the Start menu if launched often. /// MostUsed = 32, } /// /// More parameters for . /// /// /// Implicit conversion from string sets . /// public class ROptions { /// /// Sets . /// public static implicit operator ROptions(string curDir) => new ROptions { CurrentDirectory = curDir }; /// /// Initial current directory for the new process. /// If null (default), the new process will inherit the current directory of this process. /// If "", the function gets parent directory path from the file parameter, if possible (if full path is specified or found); if not possible, same as null. /// public string CurrentDirectory; /// /// File's right-click menu command, also known as verb. For example "edit", "print", "properties". /// The default verb is bold in the menu. /// Not all menu items will work. Some may have different name than in the menu. /// public string Verb; /// /// Owner window for error message boxes. /// Also, new window should be opened on the same screen, but many programs ignore it. /// public AnyWnd OwnerWindow; /// /// Preferred window state. /// Many programs ignore it. /// public ProcessWindowStyle WindowState; /// /// Flags to add to SHELLEXECUTEINFO field fMask. /// Default flags: SEE_MASK_NOZONECHECKS, SEE_MASK_NOASYNC, SEE_MASK_NOCLOSEPROCESS, SEE_MASK_CONNECTNETDRV, SEE_MASK_UNICODE, SEE_MASK_FLAG_NO_UI (if no flag ShowErrorUI), SEE_MASK_NO_CONSOLE (if no flag WaitForExit), SEE_MASK_FLAG_LOG_USAGE (if flag MostUsed); also SEE_MASK_INVOKEIDLIST if need. /// public uint FlagsAdd; /// /// Flags to remove from SHELLEXECUTEINFO field fMask. /// Default flags: see . /// public uint FlagsRemove; //no. If need, caller can get window and call EnsureInScreen etc. //public screen Screen; //this either does not work or I could not find a program that uses default window position (does not save/restore) //if(!more.Screen.IsNull) { x._14.hMonitor = more.Screen.ToDevice().Handle; x.fMask |= Api.SEE_MASK_HMONITOR; } } /// /// Results of . /// public class RResult { /// /// The exit code of the process. /// /// 0 if no flag WaitForExit or if cannot wait. /// /// Usually the exit code is 0 or a process-defined error code. /// public int ProcessExitCode { get; internal set; } /// /// The process id. /// /// 0 if used flag WaitForExit or if did not start new process (eg opened the document in an existing process) or if cannot get it. public int ProcessId { get; internal set; } /// /// If used flag NeedProcessHandle, contains process handle. Later the variable must be disposed. /// /// null if no flag or if did not start new process (eg opened the document in an existing process) or if cannot get it. /// /// This code does the same as run.it(@"notepad.exe", flags: SRFlags.WaitForExit); /// /// public WaitHandle ProcessHandle { get; internal set; } /// /// Returns as string. /// public override string ToString() => ProcessId.ToString(); } } ================================================ FILE: Au/System/sound.cs ================================================ using Microsoft.Win32; namespace Au { /// /// Plays short sounds and speaks text. /// public static class sound { /// /// Gets or sets the sound volume of this program. Percent 0-100 of the master volume. /// /// /// /// Used for speech and .wav files, but not for system sounds. /// Sets volume for each program seperately. The program remembers it after restarting. Note: all scripts with role miniProgram (default) run in the same program (Au.Task.exe). /// public static int volume { get { Api.waveOutGetVolume(default, out var v); v &= 0xffff; return (int)Math.Round((double)v * 100 / 0xffff); } set { uint v = (uint)value; if (v > 100) throw new ArgumentException("Must be 0-100."); v = (uint)(0xffff * v / 100); Api.waveOutSetVolume(default, v << 16 | v); } } /// /// Plays a custom sound (.wav file). /// /// .wav file. /// Don't wait until the sound ends. Note: the sound ends when this process exits. /// Use the sound volume channel "System Sounds". Then isn't used. public static bool playWav(string wavFile, bool async = false, bool system = false) { var s = wavFile.NE() ? null : pathname.expand(wavFile); var f = Api.SND_FILENAME | Api.SND_NODEFAULT; if (async) f |= Api.SND_ASYNC; if (system) f |= Api.SND_SYSTEM; return Api.PlaySound(s, default, f); } /// /// Plays a system event sound. /// /// Sound event name. If null, displays all available names. /// Don't wait until the sound ends. Note: the sound ends when this process exits. /// Use the sound volume channel "System Sounds". Then isn't used. /// Play default sound if the specified sound not found or does not have a .wav file assigned. /// /// Sounds can be changed in the Control Panel's Sound dialog. /// public static bool playEvent(string name, bool async = false, bool system = false, bool orDefault = false) { if (name == null) { using var k1 = Registry.CurrentUser.OpenSubKey(@"AppEvents\Schemes\Apps\.Default"); foreach (var s in k1.GetSubKeyNames()) { using var k2 = k1.OpenSubKey(s + @"\.Current"); if (k2?.GetValue("") is string file && file.Length > 0) { var label = Registry.GetValue(@"HKEY_CURRENT_USER\AppEvents\EventLabels\" + s, "", null) as string; print.it($"{s,-30} {label,-30} {file}"); } } return false; } else { uint f = Api.SND_ALIAS; //f |= Api.SND_APPLICATION; //doesn't work. Plays only sounds from the ".Default" key, with or without this flag. if (!orDefault) f |= Api.SND_NODEFAULT; if (async) f |= Api.SND_ASYNC; if (system) f |= Api.SND_SYSTEM; return Api.PlaySound(name, default, f); } } /// /// Plays the system default sound. /// /// /// Does not wait until the sound ends. The sound can continue even when this process ends. /// public static void playDefault() { Api.MessageBeep(0x40); } /// /// Plays the system error sound. /// /// /// Does not wait until the sound ends. The sound can continue even when this process ends. /// public static void playError() { Api.MessageBeep(0x10); } //other system sounds now are silent or same as default. /// /// Generates sound of specified frequency and duration. Waits until it ends. /// /// Frequency, 37-32767 hertz. /// Duration, in milliseconds. /// Don't wait. Note: the sound ends when this process exits. public static void beep(int freq, int duration, bool async = false) { if (async) { Task.Run(() => Api.Beep(freq, duration)); } else { Api.Beep(freq, duration); } } /// /// Speaks text. /// /// Text to speak. If null, stops speaking. /// Don't wait. Note: the sound ends when this process exits. /// /// A voice name from Control Panel > Speech > Text to speech. Can be partial, case-insensitive. Example: "Zira". /// If null, uses default voice. /// Voice attributes can be specified using string format "voice|reqAttr" or "voice|reqAttr|optAttr". Here reqAttr and optAttr are arguments for ISpObjectTokenCategory.EnumTokens. Each part can be empty. Example: "|Gender=Female". /// /// Speed adjustment, +- 10. /// Volume, 0-100. See also . /// #if true public static void speak(string text, bool async = false, string voice = null, int rate = 0, int volume = 100) { lock (s_lock) { s_voice?.Stop(); if (text.NE()) return; s_voice ??= new SpeakVoice(); if (voice != s_sVoice) s_voice.SetVoice_(s_sVoice = voice); s_voice.Rate = rate; s_voice.Volume = volume; } s_voice.Speak(text, async); } static string s_sVoice; #else //use new SpVoice each time public static void speak(string text, bool async = false, string voice = null, int rate = 0, int volume = 100) { SpeakVoice v = null; lock (s_lock) { if (s_voice!=null) { s_voice.Dispose(); s_voice=null; } if (text.NE()) return; v = new SpeakVoice(voice); if (rate != 0) v.Rate = rate; if (volume != 100) v.Volume = volume; s_voice=v; } v.Speak(text, async); if(!async) { lock (s_lock) { if (s_voice==v) { s_voice=null; v.Dispose(); } } } //else v.EndStream+=(o, e) => print.it("end"); //need to process messages } #endif static SpeakVoice s_voice; static readonly object s_lock = new(); } } namespace Au.More { /// /// Speaks text. /// /// public class SpeakVoice : IDisposable { SAPI.ISpVoice _v; /// /// Creates a text-to-speech (speech synthesis) voice instance. /// /// A voice name from Control Panel > Speech > Text to speech. Can be partial, case-insensitive. Example: "Zira". If null, uses default voice. public SpeakVoice(string voice = null) { _v = new SAPI.SpVoice() as SAPI.ISpVoice; GC.AddMemoryPressure(250_000); if (voice != null) SetVoice_(voice); } internal void SetVoice_(string voice) { if (!voice.NE()) { var cat = new SAPI.SpObjectTokenCategory() as SAPI.ISpObjectTokenCategory; cat.SetId(SAPI.SPCAT_VOICES, false); var a = voice.Split('|'); voice = a[0]; var et = cat.EnumTokens(a.Length > 1 && !a[1].NE() ? a[1] : null, a.Length > 2 && !a[2].NE() ? a[2] : null); for (int i = 0, n = et.GetCount(); i < n; i++) { var v = et.Item(i); if (!voice.NE()) { if (0 != v.OpenKey("Attributes", out var k)) continue; if (0 != k.GetStringValue("Name", out var s)) continue; if (s.Find(voice, true) < 0) continue; } _v.SetVoice(v); break; } } else _v.SetVoice(null); } /// protected virtual void Dispose(bool disposing) { if (_v != null) { if (disposing) Marshal.ReleaseComObject(_v); //stops speaking if async _v = null; GC.RemoveMemoryPressure(250_000); } } /// public void Dispose() { Stop(); Dispose(true); GC.SuppressFinalize(this); } /// ~SpeakVoice() { //print.it("~"); Dispose(false); } /// /// Gets or sets the speed adjustment, +- 10. /// public int Rate { get => _v.GetRate(); set { _v.SetRate(value); } } /// /// Gets or sets the volume, 0-100. See also . /// public int Volume { get => _v.GetVolume(); set { _v.SetVolume((ushort)value); } } /// /// Pauses speaking. /// public void Pause() => _v.Pause(); /// /// Resumes speaking. /// public void Resume() => _v.Resume(); /// /// Skips count milliseconds of speech. /// /// Forward if positive, else backward. public void SkipMilliseconds(int count) => _v.Skip("MILLISECOND", count); /// /// Skips count sentences of speech. /// /// Forward if positive, else backward. If 0, repeats current sentence. public void SkipSentence(int count) => _v.Skip("SENTENCE", count); /// /// Stops speaking. /// public void Stop() => SkipSentence(int.MaxValue); /// /// Returns true if currently is speaking. Returns false if finished or not started. /// public bool IsSpeaking => _RunningState() == SAPI.SpeechRunState.SRSEIsSpeaking; /// /// Returns true if finished speaking. /// public bool IsDone => _RunningState() == SAPI.SpeechRunState.SRSEDone; SAPI.SpeechRunState _RunningState() { _v.GetStatus(out var r); return r.RunningState; } /// /// Waits until the async speech ends. /// /// Timeout milliseconds, or -1. public bool WaitUntilDone(int msTimeout) => 0 == _v.WaitUntilDone(msTimeout); /// /// Speaks the specified text. /// /// Text to speak. /// Don't wait. Note: the sound ends when this process exits. public void Speak(string text, bool async = false) { Speak(text, async ? SVFlags.ASYNC : 0); } /// /// Speaks the specified text. /// /// Text to speak. /// public void Speak(string text, SVFlags flags) { if (flags.Has(SVFlags.IS_FILENAME)) text = pathname.expand(text); _v.Speak(text, (uint)flags); GC.KeepAlive(this); if (flags.Has(SVFlags.ASYNC)) { //protect from GC while speaking Task.Run(() => { _v.WaitUntilDone(-1); GC.KeepAlive(this); }); } } } //Easier would be to use the SAPI type library, but it creates problems, eg dotnet pack fails. unsafe class SAPI : NativeApi { [ComImport, Guid("6C44DF74-72B9-4992-A1EC-EF996E0422D4"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface ISpVoice { void _0(); void _1(); void _2(); void _3(); void _4(); void _5(); void _6(); void _7(); void _8(); void _9(); void _a(); void _b(); void _c(); void Pause(); void Resume(); void SetVoice(ISpObjectToken pToken); void _d(); int Speak([MarshalAs(UnmanagedType.LPWStr)] string pwcs, uint dwFlags); void _e(); void GetStatus(out SPVOICESTATUS pStatus, nint ppszLastBookmark = 0); int Skip([MarshalAs(UnmanagedType.LPWStr)] string pItemType, int lNumItems); void _f(); void _g(); void _h(); void _i(); void SetRate(int RateAdjust); int GetRate(); void SetVolume(ushort usVolume); ushort GetVolume(); [PreserveSig] int WaitUntilDone(int msTimeout); } internal struct SPVOICESTATUS { fixed uint _1[3]; public SpeechRunState RunningState; fixed uint _2[9]; } [ComImport, Guid("14056589-E16C-11D2-BB90-00C04F8EE6C0"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface ISpObjectToken { void _0(); void _1(); void _2(); void _3(); void _4(); void _5(); [PreserveSig] int OpenKey([MarshalAs(UnmanagedType.LPWStr)] string pszSubKeyName, out ISpDataKey ppSubKey); } [ComImport, Guid("2D3D3845-39AF-4850-BBF9-40B49780011D"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface ISpObjectTokenCategory { void _0(); void _1(); void _2(); void _3(); void _4(); void _5(); void _6(); void _7(); void _8(); void _9(); void _a(); void _b(); void SetId([MarshalAs(UnmanagedType.LPWStr)] string pszCategoryId, [MarshalAs(UnmanagedType.Bool)] bool fCreateIfNotExist); void _c(); void _d(); IEnumSpObjectTokens EnumTokens([MarshalAs(UnmanagedType.LPWStr)] string pzsReqAttribs, [MarshalAs(UnmanagedType.LPWStr)] string pszOptAttribs); } [ComImport, Guid("14056581-E16C-11D2-BB90-00C04F8EE6C0"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface ISpDataKey { void _0(); void _1(); void _2(); [PreserveSig] int GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string pszValueName, [MarshalAs(UnmanagedType.LPWStr)] out string s); } [ComImport, Guid("06B64F9E-7FDA-11D2-B4F2-00C04F797396"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IEnumSpObjectTokens { void _0(); void _1(); void _2(); void _3(); ISpObjectToken Item(int Index); int GetCount(); } [ComImport, Guid("96749377-3391-11D2-9EE3-00C04F797396"), ClassInterface(ClassInterfaceType.None)] internal class SpVoice { } internal enum SpeechRunState { SRSEDone = 1, SRSEIsSpeaking } [ComImport, Guid("A910187F-0C7A-45AC-92CC-59EDAFB77B53"), ClassInterface(ClassInterfaceType.None)] internal class SpObjectTokenCategory { } internal const string SPCAT_VOICES = "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices"; } } namespace Au.Types { /// /// Flags for . See SPEAKFLAGS. /// [Flags] public enum SVFlags { #pragma warning disable 1591 //XML doc ASYNC = 0x0001, PURGEBEFORESPEAK = 0x0002, IS_FILENAME = 0x0004, IS_XML = 0x0008, IS_NOT_XML = 0x0010, PERSIST_XML = 0x0020, NLP_SPEAK_PUNC = 0x0040, PARSE_SAPI = 0x0080, PARSE_SSML = 0x0100, #pragma warning restore } } ================================================ FILE: Au/System/uacInfo.cs ================================================ namespace Au { /// /// Gets [](xref:uac) integrity level and other security info of this and other processes. /// /// /// An uacInfo variable contains a process access token handle that is used to get security info. Always dispose uacInfo variables to close the handle. /// public sealed class uacInfo : IDisposable { /// ~uacInfo() => _htoken.Dispose(); /// public void Dispose() { _htoken.Dispose(); GC.SuppressFinalize(this); } Handle_ _htoken; HandleRef _HtokenHR => new HandleRef(this, _htoken); /// /// The access token handle. /// /// /// The handle is managed by this variable and will be closed when disposing or GC-collecting it. Use where need. /// public IntPtr UnsafeTokenHandle => _htoken; /// /// Returns true if the last called property function failed. /// Normally it should never fail. Only can fail (then it returns null). /// public bool Failed { get; private set; } /// /// Gets the [](xref:uac) elevation type of the process. /// public UacElevation Elevation { get { if (_haveElevation == 0) { unsafe { UacElevation elev; if (!Api.GetTokenInformation(_HtokenHR, Api.TOKEN_INFORMATION_CLASS.TokenElevationType, &elev, 4, out _)) _haveElevation = 2; else { _haveElevation = 1; _Elevation = elev; } } } if (Failed = (_haveElevation == 2)) return UacElevation.Unknown; return _Elevation; } } UacElevation _Elevation; byte _haveElevation; /// /// Returns true if the process has [](xref:uac) uiAccess property. /// A uiAccess process can access/automate all windows of processes running in the same user session. /// /// /// Most processes don't have this property. They cannot access/automate windows of higher integrity level (High, System, uiAccess) processes and Windows 8 store apps. For example, cannot send keys and Windows messages. /// Note: High IL (admin) processes also can have this property, therefore IsUIAccess is not the same as IntegrityLevel==IL.UIAccess ( returns UIAccess only for Medium+uiAccess processes; for High+uiAccess processes it returns High). Some Windows API work slightly differently with uiAccess and non-uiAccess admin processes. /// This property is rarely useful. Instead use other properties of this class. /// public bool IsUIAccess { get { if (_haveIsUIAccess == 0) { unsafe { uint uia; if (!Api.GetTokenInformation(_HtokenHR, Api.TOKEN_INFORMATION_CLASS.TokenUIAccess, &uia, 4, out var siz)) _haveIsUIAccess = 2; else { _haveIsUIAccess = 1; _isUIAccess = uia != 0; } } } if (Failed = (_haveIsUIAccess == 2)) return false; return _isUIAccess; } } bool _isUIAccess; byte _haveIsUIAccess; //not very useful. Returns false for ApplicationFrameWindow. Can use wnd.IsWindows10StoreApp. ///// ///// Returns true if the process is a Windows Store app. ///// //public unsafe bool IsAppContainer //{ // get // { // if(!osVersion.minWin8) return false; // uint isac; // if(Failed = !Api.GetTokenInformation(_HtokenHR, Api.TOKEN_INFORMATION_CLASS.TokenIsAppContainer, &isac, 4, out var siz)) return false; // return isac != 0; // } //} static class _Api { #pragma warning disable 649 internal struct TOKEN_MANDATORY_LABEL { public IntPtr Sid; public uint Attributes; } #pragma warning restore 649 internal const uint SECURITY_MANDATORY_UNTRUSTED_RID = 0x00000000; internal const uint SECURITY_MANDATORY_LOW_RID = 0x00001000; internal const uint SECURITY_MANDATORY_MEDIUM_RID = 0x00002000; internal const uint SECURITY_MANDATORY_MEDIUM_PLUS_RID = SECURITY_MANDATORY_MEDIUM_RID + 0x100; internal const uint SECURITY_MANDATORY_HIGH_RID = 0x00003000; internal const uint SECURITY_MANDATORY_SYSTEM_RID = 0x00004000; internal const uint SECURITY_MANDATORY_PROTECTED_PROCESS_RID = 0x00005000; } /// /// Gets the [](xref:uac) integrity level (IL) of the process. /// /// /// IL from lowest to highest value: Untrusted, Low, Medium, UIAccess, High, System, Protected, Unknown. /// The IL enum member values can be used like if(x.IntegrityLevel > IL.Medium) ... . /// If UAC is turned off, most non-service processes on administrator account have High IL; on non-administrator - Medium. /// public UacIL IntegrityLevel => _GetIntegrityLevel(); UacIL _GetIntegrityLevel() { if (_haveIntegrityLevel == 0) { unsafe { Api.GetTokenInformation(_HtokenHR, Api.TOKEN_INFORMATION_CLASS.TokenIntegrityLevel, null, 0, out var siz); if (lastError.code != Api.ERROR_INSUFFICIENT_BUFFER) _haveIntegrityLevel = 2; else { var b = stackalloc byte[(int)siz]; var tml = (_Api.TOKEN_MANDATORY_LABEL*)b; if (!Api.GetTokenInformation(_HtokenHR, Api.TOKEN_INFORMATION_CLASS.TokenIntegrityLevel, tml, siz, out siz)) _haveIntegrityLevel = 2; uint x = *Api.GetSidSubAuthority(tml->Sid, (uint)(*Api.GetSidSubAuthorityCount(tml->Sid) - 1)); if (x < _Api.SECURITY_MANDATORY_LOW_RID) _integrityLevel = UacIL.Untrusted; else if (x < _Api.SECURITY_MANDATORY_MEDIUM_RID) _integrityLevel = UacIL.Low; else if (x < _Api.SECURITY_MANDATORY_HIGH_RID) _integrityLevel = UacIL.Medium; else if (x < _Api.SECURITY_MANDATORY_SYSTEM_RID) { if (IsUIAccess && Elevation != UacElevation.Full) _integrityLevel = UacIL.UIAccess; //fast. Note: don't use if(andUIAccess) here. else _integrityLevel = UacIL.High; } else if (x < _Api.SECURITY_MANDATORY_PROTECTED_PROCESS_RID) _integrityLevel = UacIL.System; else _integrityLevel = UacIL.Protected; } } } if (Failed = (_haveIntegrityLevel == 2)) return UacIL.Unknown; return _integrityLevel; } UacIL _integrityLevel; byte _haveIntegrityLevel; uacInfo(Handle_ hToken) => _htoken = hToken; static uacInfo _Create(IntPtr hProcess) { if (!Api.OpenProcessToken(hProcess, Api.TOKEN_QUERY | Api.TOKEN_QUERY_SOURCE, out Handle_ hToken)) return null; return new uacInfo(hToken); } /// /// Opens process access token and creates/returns new variable that holds it. Then you can use its properties. /// /// null if failed. For example fails for services and some other processes if current process is not administrator. /// Process id. If you have a window, use . /// /// To get of this process, use . /// public static uacInfo ofProcess(int processId) { if (processId == 0) return null; using var hp = Handle_.OpenProcess(processId); if (hp.Is0) return null; return _Create(hp); } /// /// Gets variable for this process. /// public static uacInfo ofThisProcess { get { if (s_thisProcess == null) { s_thisProcess = _Create(Api.GetCurrentProcess()); Debug.Assert(s_thisProcess != null); } return s_thisProcess; } } static uacInfo s_thisProcess; /// /// Returns true if this process is running as administrator. /// #if true public static bool isAdmin => s_isAdmin ??= Api.IsUserAnAdmin(); #else //too slow, eg 15 ms vs 1 ms public static bool IsAdmin { get { if(!s_isAdmin.HasValue) { try { WindowsIdentity id = WindowsIdentity.GetCurrent(); WindowsPrincipal principal = new WindowsPrincipal(id); s_isAdmin = principal.IsInRole(WindowsBuiltInRole.Administrator); } catch { } } return s_isAdmin.GetValueOrDefault(); } } #endif static bool? s_isAdmin; /* public struct SID_IDENTIFIER_AUTHORITY { public byte b0, b1, b2, b3, b4, b5; } [DllImport("advapi32.dll")] internal static extern bool AllocateAndInitializeSid(in SID_IDENTIFIER_AUTHORITY pIdentifierAuthority, byte nSubAuthorityCount, uint nSubAuthority0, uint nSubAuthority1, uint nSubAuthority2, uint nSubAuthority3, uint nSubAuthority4, uint nSubAuthority5, uint nSubAuthority6, uint nSubAuthority7, out IntPtr pSid); [DllImport("advapi32.dll")] internal static extern bool CheckTokenMembership(IntPtr TokenHandle, IntPtr SidToCheck, out bool IsMember); [DllImport("advapi32.dll")] internal static extern IntPtr FreeSid(IntPtr pSid); public const int SECURITY_BUILTIN_DOMAIN_RID = 32; public const int DOMAIN_ALIAS_RID_ADMINS = 544; //This is from CheckTokenMembership reference. //In QM2 it is very fast, but here quite slow first time, although then becomes the fastest. Advapi32.dll is already loaded, but maybe it loads other dlls. //IsUserAnAdmin first time can be slowest. It loads shell32.dll. //The .NET principal etc first time usually is fastest, althoug later is slower several times. (old info) //All 3 tested on admin and user accounts, also when UAC is turned off, also with System IL. public static bool isAdmin { get { var NtAuthority = new SID_IDENTIFIER_AUTHORITY() { b5 = 5 }; //SECURITY_NT_AUTHORITY IntPtr AdministratorsGroup; if(!AllocateAndInitializeSid(NtAuthority, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, out AdministratorsGroup )) return false; bool _r; if(!CheckTokenMembership(default, AdministratorsGroup, out _r)) _r = false; FreeSid(AdministratorsGroup); return _r; } } */ /// /// Returns true if [](xref:uac) is disabled (turned off) completely (not just disabled UAC consent screen/dialog). /// public static bool isUacDisabled { get { if (!_haveIsUacDisabled) { _isUacDisabled = _IsUacDisabled(); _haveIsUacDisabled = true; } return _isUacDisabled; } } static bool _isUacDisabled, _haveIsUacDisabled; static bool _IsUacDisabled() { //if(osVersion.minWin8) return false; //UAC cannot be disabled so easily, but can uacInfo x = ofThisProcess; switch (x.Elevation) { case UacElevation.Full: case UacElevation.Limited: return false; } //if(x.IsUIAccess) return false; //uiAccess in non-admin user session. Rare. int r = 1; try { if (!Api.GetDelegate(out Api.CheckElevationEnabled d, "kernel32.dll", "CheckElevationEnabled") || 0 != d(out r)) { Debug_.Print("CheckElevationEnabled"); r = Microsoft.Win32.Registry.GetValue(@"HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System", "EnableLUA", null) is int v ? v : 1; } } catch (Exception e) { Debug_.Print(e); } return r == 0; } } } namespace Au.Types { /// /// UAC integrity level. /// See . /// public enum UacIL { /// The most limited rights. Rare. Untrusted, /// Very limited rights. Used by web browser tab processes, Windows Store apps. Low, /// Limited rights. Most processes (unless UAC turned off). Medium, /// Medium IL + can access/automate High IL windows (user interface). UIAccess, /// Most rights. Processes that run as administrator. High, /// Almost all rights. Services, some system processes. System, /// Undocumented. Rare. Protected, /// Failed to get IL. Unlikely. Unknown = 100 } /// /// . /// public enum UacElevation { /// Failed to get. Normally it never happens. Unknown, /// /// Processes in this user session cannot be elevated. /// Can be: non-administrator user session (processes have limited rights); service session (processes have all rights); UAC is turned off (most processes have administrator rights). /// Default, /// Runs as administrator (High or System integrity level, see ), and UAC is not turned off. Also known as "elevated". Full, /// Runs as standard user (Medium, UIAccess or Low integrity level, see ) in administrator user session (because of UAC). Limited } } ================================================ FILE: Au/Time/WaitLoop.cs ================================================ namespace Au.More { /// /// Can be used to easily implement "wait for" functions with a timeout. /// /// /// See examples. The code works like most "wait for" functions of this library: throws exception when timed out, unless the timeout value is negative. /// Similar code is used by and many other "wait for" functions of this library. /// /// /// /// The same with . /// mouse.isPressed(MButtons.Left)); /// } /// ]]> /// public struct WaitLoop { long _timeRemaining, _timePrev; bool _hasTimeout, _throw, _doEvents, _precisionIsSet; float _step; /// /// Sets timeout and possibly more wait parameters. /// /// Timeout in seconds, like 3 or 0.5. Or a Seconds variable containing timeout etc, like new(3, period: 5). If timeout is 0, will wait indefinitely. If > 0, throws when timed out. If < 0, Sleep then returns false instead. public WaitLoop(Seconds timeout) { Period = timeout.Period ?? 10; _step = Period / 10f; MaxPeriod = timeout.MaxPeriod ?? Period * 50f; _doEvents = timeout.DoEvents ?? opt.wait.DoEvents; //use opt.wait fbc Cancel = timeout.Cancel; double t = timeout.Time; if (_hasTimeout = !(t is 0d or > 9223372036854775d or < -9223372036854775d)) { //long.MaxValue/1000 = 292_471_208 years _throw = t > 0 && !timeout.noException_; _timeRemaining = (long)(Math.Abs(t) * 1000d); _timePrev = computer.tickCountWithoutSleep; } } /// Timeout in seconds. If 0, will wait indefinitely. If > 0, throws when timed out. If < 0, Sleep then returns false instead. /// Options. If null, uses . [Obsolete, EditorBrowsable(EditorBrowsableState.Never)] public WaitLoop(double secondsTimeout, OWait options) : this(new(secondsTimeout) { Period = (options ?? opt.wait).Period, DoEvents = (options ?? opt.wait).DoEvents }) { } /// /// Current period ( sleep time). Milliseconds. /// Initially it is , or 10 ms if it was null. Then each increments it until . /// public float Period { get; set; } /// /// Maximal period ( sleep time). Milliseconds. /// Initially it is , or * 50 if it is null (eg 10*50=500). /// public float MaxPeriod { get; set; } /// /// Gets or sets the remaining time. Milliseconds. /// public long TimeRemaining { get => _timeRemaining; set => _timeRemaining = value; } /// /// Calls . If it returns true, returns false. /// Else sleeps milliseconds, increments Period if it is less than , and returns true. /// /// The timeout time has expired (if > 0). public unsafe bool Sleep() { if (IsTimeout()) return false; int t = (int)Period; if (t < 10 && !_precisionIsSet) { //default Period is 10 _precisionIsSet = true; wait.SleepPrecision_.TempSet1(); } if (Cancel.CanBeCanceled && t > 100) { //t > 100: avoid creating Cancel.WaitHandle while period is small. By default will create after ~5 s. var h = Cancel.WaitHandle.SafeWaitHandle.DangerousGetHandle(); int r = wait.Wait_(t, _doEvents ? WHFlags.DoEvents : 0, new ReadOnlySpan(&h, 1)); if (r == 0) Cancel.ThrowIfCancellationRequested(); if (r != Api.WAIT_TIMEOUT) throw new AuException(0); } else { Cancel.ThrowIfCancellationRequested(); if (_doEvents) { wait.doEvents(t); } else { Thread.Sleep(t); } Cancel.ThrowIfCancellationRequested(); } if (Period < MaxPeriod) Period += _step; return true; } /// /// If the timeout time is not expired, returns false. /// Else if the timeout was negative, returns true. /// Else throws . /// /// public bool IsTimeout() { if (!_hasTimeout) return false; var t = computer.tickCountWithoutSleep; _timeRemaining -= t - _timePrev; _timePrev = t; if (_timeRemaining > 0) return false; if (_throw) throw new TimeoutException(); return true; } /// /// Can be used to cancel the wait operation. /// public CancellationToken Cancel { get; set; } } } namespace Au.Types { /// /// Used with wait functions. Contains a wait timeout in seconds, and possibly wait options. /// /// /// Many wait functions of this library internally use . They have a timeout parameter of Seconds type which allows to pass timeout and more options to WaitLoop in single parameter. You can pass a Seconds variable, like new(3, period: 5). If don't need options etc, you can pass just timeout, like 3 or 0.5. /// /// Other wait functions have a timeout parameter of Seconds type but instead of WaitLoop use various hooks, events, Windows wait API, etc. They support only these Seconds properties: Time, Cancel, maybe DoEvents. Some always work like with DoEvents true. /// /// More info: [](xref:wait_timeout). /// /// /// /// keys.isCtrl ); /// wait.until(to with { Time = 30 }, () => keys.isCtrl ); /// ]]> /// public struct Seconds { float _time; int _period, _maxPeriod; byte _doEvents; //bool _inited; internal bool noException_; //Now Unsafe.SizeOf is 24. // Don't use bool? etc. Nullable takes at least 8 bytes if the struct also has a managed-type field. /// /// Sets timeout. /// Example: var w = wnd.find(new Seconds(3) { Period = 5, MaxPeriod = 50 }, "Name");. /// /// /// Timeout, in seconds. /// Negative value means "don't throw exception when timed out". /// Value 0 means "wait indefinitely" when used with WaitX functions; with FindX functions it means "don't wait". /// More info: [](xref:wait_timeout). /// public Seconds(double time) { _time = (float)time; //_inited = true; } /// public static implicit operator Seconds(double time) => new(time); /// /// Timeout, in seconds. /// Negative value means "don't throw exception when timed out". /// Value 0 means "wait indefinitely" when used with WaitX functions; with FindX functions it means "don't wait". /// More info: [](xref:wait_timeout). /// public double Time { get => _time; set { _time = (float)value; } } /// /// The sleep time between checking the wait condition periodically. Milliseconds. /// If null, will be used a value that usually is best for that wait function, in most cases 10. /// /// /// Most wait functions of this library use , which repeatedly checks the wait condition and sleeps (waits) several ms. This property sets the initial sleep time (), which then is incremented by Period/10 ms in each loop until reaches , which is Period*50 by default. /// This property makes the response time shorter or longer. If less than the default value, makes it shorter (faster response), but increases CPU usage; if greater, makes it longer (slower response). /// public int? Period { get => _period != 0 ? _period : null; set { _period = value.HasValue ? Math.Max(1, value.Value) : 0; } } /// /// Sets . /// If null (default), it will use Period*50. /// public int? MaxPeriod { get => _maxPeriod != 0 ? _maxPeriod : null; set { _maxPeriod = value.HasValue ? Math.Max(1, value.Value) : 0; } } /// /// Use instead of . /// If null, will be used false. /// public bool? DoEvents { get => _doEvents != 0 ? _doEvents == 1 : null; set { _doEvents = value switch { true => 1, false => 2, _ => 0 }; } } /// /// Can be used to cancel the wait operation. /// public CancellationToken Cancel { get; set; } ///// ///// Returns true if ctor wasn't called. ///// //internal bool IsNull_ => !_inited; /// /// Sets noException_ = true and returns Time == 0d. /// internal bool Exists_() { noException_ = true; return _time == 0d; } /// /// If Time < 0, returns false, else throws . /// internal bool ReturnFalseOrThrowNotFound_() => Time < 0 ? false : throw new NotFoundException(); //internal T ReturnOrThrowNotFound_(T def) => Time < 0 ? def : throw new NotFoundException(); } } ================================================ FILE: Au/Time/perf.cs ================================================ //#define PREPAREMETHOD //now slow anyway. Does not JIT everything. namespace Au { /// /// Code execution time measurement with high precision (API QueryPerformanceCounter). /// public static unsafe class perf { //info: we don't use Stopwatch. It used to load System.dll (before .NET Core, now don't know), which is slow and can make speed measurement incorrect and confusing in some cases. static readonly double s_freqMCS = Api.QueryPerformanceFrequency(out long f) ? 1_000_000d / f : 0d, s_freqMS = s_freqMCS / 1000d; //note: don't use static ctor. Makes some simplest functions much slower. /// /// Gets the number of microseconds elapsed since Windows startup. Uses the high-resolution system timer (API QueryPerformanceCounter). /// /// /// This function is used to measure time differences with 1 microsecond precision, like var t1=perf.mcs; ... var t2=perf.mcs; var diff=t2-t1;. /// Independent of computer clock time changes. /// See also: Acquiring high-resolution time stamps. /// /// public static long mcs { get { Api.QueryPerformanceCounter(out var t); return (long)(t * s_freqMCS); } } //On current OS and hardware QueryPerformanceCounter is reliable and fast enough. //Speed of 1000 calls when cold CPU: PerfMilliseconds 97, WinMillisecondsWithoutSleep 76, WinMilliseconds64/GetTickCount64 12. //The double cast/multiply/cast is fast. The API is much much slower. And Math.BigMul is much slower. //rejected: make corrections based on GetTickCount64. It makes slower and is not necessary. /// /// Gets the number of milliseconds elapsed since Windows startup. Uses the high-resolution system timer (API QueryPerformanceCounter). /// /// /// This function is used to measure time differences with 1 ms precision, like var t1=perf.ms; ... var t2=perf.ms; var diff=t2-t1;. /// The return value equals /1000 but is slightly different than of and most Windows API. Never compare times returned by different functions. /// Independent of computer clock time changes. /// public static long ms { get { Api.QueryPerformanceCounter(out var t); return (long)(t * s_freqMS); } } /// /// Performs time measurements and stores measurement data. /// /// /// Static functions of class use a single static variable of this type to perform measurements. /// Use a variable of this type instead when you want to have multiple independent measurements. See . /// Variables of this type usually are used as local (in single function), but also can be used anywhere (class fields, unmanaged memory). /// This type is a struct (value type), and not small (184 bytes). Don't use it as a function parameter without ref. To share a variable between functions use a field in your class. /// Don't need to dispose variables of this type. The function just calls . /// public unsafe struct Instance : IDisposable { static Instance() { //Prevent JIT delay when calling Next etc #if PREPAREMETHOD Jit_.Compile(typeof(Instance), "Next", "NW", "Dispose"); #if DEBUG //else these methods are inlined Jit_.Compile(typeof(perf), "Next", "NW"); #endif #else perf.next(); perf.nw(); perf.first(); //JIT-compiles everything we need. s_enabled prevents calling print.it etc. s_enabled = true; #endif //JIT speed: 1 ms. } #if !PREPAREMETHOD readonly static bool s_enabled; #endif const int _nElem = 16; fixed long _a[_nElem]; fixed char _aMark[_nElem]; volatile int _counter; bool _incremental; int _nMeasurements; //used with incremental to display n measurements and average times long _time0; /// See . /// /// /// public bool Incremental { get => _incremental; set { if (_incremental = value) { fixed (long* p = _a) { for (int i = 0; i < _nElem; i++) p[i] = 0; } _nMeasurements = 0; } } } /// public void First() { #if !PREPAREMETHOD if (!s_enabled) return; //called by the static ctor #endif Api.QueryPerformanceCounter(out _time0); _counter = 0; _nMeasurements++; } //rejected. See comments in static. ///// ///// Calls and . ///// //public void First(int timeSpeedUpCPU) //{ // Cpu(timeSpeedUpCPU); // First(); //} /// public void Next(char cMark = '\0') { #if !PREPAREMETHOD if (!s_enabled) return; //called by the static ctor #endif int n = _counter; if (n >= _nElem) return; _counter++; fixed (long* p = _a) { Api.QueryPerformanceCounter(out long pc); long t = pc - _time0; if (_incremental) p[n] += t; else p[n] = t; //fixed (char* c = _aMark) c[n] = cMark; char* c = (char*)(p + _nElem); c[n] = cMark; } } /// /// Calls and . /// /// A character to add to the results string like "A=150". [MethodImpl(MethodImplOptions.NoInlining)] public void NW(char cMark = '\0') { Next(cMark); Write(); } /// /// Calls , which calls and . /// /// /// Don't need to dispose variables of this type. This function just allows to use using instead of . See example. /// /// If , calls just . /// /// /// /// public void Dispose() { if (!_incremental) Next(); Write(); } //public void Dispose() => NW(); /// /// Formats a string from time values collected by calling and , and shows it in the output. /// The string contains the number of microseconds of each code execution between calling First and each Next. /// public void Write() { #if !PREPAREMETHOD if (!s_enabled) return; //called by the static ctor #endif print.it(ToString()); } /// /// Formats a string from time values collected by calling and . /// The string contains the number of microseconds of each code execution between calling First and each Next. /// public override string ToString() { using (new StringBuilder_(out var b)) { b.Append("speed:"); _Results(Math.Min(_counter, _nElem), b, null); return b.ToString(); } } /// /// Return array of time values collected by calling and . /// Each element is the number of microseconds of each code execution between calling First and each Next. /// public long[] ToArray() { int n = Math.Min(_counter, _nElem); var a = new long[n]; _Results(n, null, a); return a; } void _Results(int n, StringBuilder b, long[] a) { if (n == 0) return; bool average = false; int nMeasurements = 1; fixed (long* p = _a) fixed (char* c = _aMark) { g1: double t = 0d, tPrev = 0d; for (int i = 0; i < n; i++) { t = s_freqMCS * p[i]; double d = t - tPrev; tPrev = t; //could add 0.5 to round up, but assume that QueryPerformanceCounter call time is 0 - 0.5. if (average) d /= nMeasurements; long dLong = (long)d; if (b != null) { b.Append(" "); if (c[i] != '\0') b.Append(c[i]).Append('='); b.Append(dLong.ToString()); } else { a[i] = dLong; } } if (b == null) return; if (n > 1) { if (average) t /= nMeasurements; b.Append(" (").Append((long)t).Append(")"); } if (!average && _incremental && (nMeasurements = _nMeasurements) > 1) { average = true; b.Append("; measured ").Append(nMeasurements).Append(" times, average"); goto g1; } } } /// /// Gets the number of microseconds between and the last . /// public long TimeTotal { get { int n = _counter; if (n == 0) return 0; if (n > _nElem) n = _nElem; fixed (long* p = _a) { return (long)(s_freqMCS * p[n - 1]); } } } } /// /// This static variable is used by the static functions. /// static Instance s_static; //rejected: public. Needed for Serialize. Maybe in the future will be implemented somehow differently. /// /// Creates and returns new variable and calls its . /// public static Instance local() { var R = new Instance(); R.First(); return R; } /// /// If true, times of each new first next next... measurement are added to previous measurement times. /// Finally you can call or to get the sums. /// Usually used to measure code in loops. See example. /// /// /// /// public static bool incremental { get => s_static.Incremental; set => s_static.Incremental = value; } /// /// Stores current time in the first element of an internal array. /// public static void first() => s_static.First(); //rejected. Unclear. Can confuse int timeSpeedUpCPU with char cMark. ///// ///// Calls and . ///// //public static void first(int timeSpeedUpCPU) => s_static.First(timeSpeedUpCPU); /// /// Stores current time in next element of an internal array. /// /// /// Don't call next more than 16 times after , because the array size is fixed. /// /// A character to mark this time in the results string, like "A=150". public static void next(char cMark = '\0') => s_static.Next(cMark); /// /// Calls and . /// /// A character to add to the results string like "A=150". public static void nw(char cMark = '\0') => s_static.NW(cMark); /// /// Formats a string from time values collected by calling and , and shows it in the output. /// The string contains the number of microseconds of each code execution between calling and each . /// /// /// /// public static void write() => s_static.Write(); /// /// Formats a string from time values collected by calling and . /// The string contains the number of microseconds of each code execution between calling and each . /// public static string toString() => s_static.ToString(); /// /// Return array of time values collected by calling and . /// Each element is the number of microseconds of each code execution between calling and each . /// public static long[] toArray() => s_static.ToArray(); /// /// Gets the number of microseconds between and the last . /// public static long timeTotal => s_static.TimeTotal; /// /// Executes some code in loop for the specified amount of time. It should make CPU to run at full speed. /// /// How long to speed up CPU, milliseconds. The minimal required time probably is about 100 ms, but depends on CPU. /// /// Code speed measurements often are misleading because of variable CPU speed. Most CPU don't run at full speed when not actively used. /// /// You can make CPU speed constant in Control Panel > Power Options > ... Advanced > Processor power management > Minimum or maximum power state. /// There are programs that show current CPU speed. For example HWMonitor. /// public static void cpu(int timeMilliseconds = 200) { int n = 0; for (long t0 = mcs; mcs - t0 < timeMilliseconds * 1000L; n++) { } //print.it(n); } /// /// Gets a reference to a variable in shared memory. /// /// /// The variable can be used by multiple processes, for example to measure process startup time. /// Note: slow first time in process, eg 3 ms. It's because need to JIT-compile functions and open shared memory. /// /// /// /// public static ref Instance shared => ref SharedMemory_.Ptr->perf; } } ================================================ FILE: Au/Time/timer.cs ================================================ namespace Au; /// /// Timer that calls callback function in same thread, which must have a message loop. /// /// /// Uses API SetTimer and WM_TIMER. /// Works only in threads that have a message loop which retrieves/dispatches posted messages. For example threads with windows (except console). /// Timer action delegates are protected from GC. /// /// /// This example sets 3 timers. /// print.it("after 500 ms")); /// timer.every(1000, _ => print.it("every 1000 ms")); /// var t3 = new timer(_ => print.it("after 3000 ms")); t3.After(3000); //the same as timer.after /// dialog.show("timer"); //shows a dialog window and waits until closed. The dialog retrieves/dispatches messages in its message loop. /// ]]> /// public class timer { readonly Action _action; nint _id; int _threadId; bool _singlePeriod; //To control object lifetime we use a thread-static Dictionary. //Tried GCHandle, but could not find a way to delete object when thread ends. //Calling KillTimer when thread ends is optional. Need just to re-enable garbage collection for this object. [ThreadStatic] static Dictionary t_timers; /// /// Sets callback function. /// public timer(Action timerAction) { _action = timerAction; } //rejected: add overload with hwnd. Or optional parameter. Or another class. Never needed in several years. /// /// Something to attach to this variable. /// public object Tag { get; set; } /// /// true if the timer is started and not stopped. /// Note: single-period timer is automatically stopped before calling the callback function. /// public bool IsRunning => _id != default; /// /// Starts one-time timer. If already started, resets and changes its period. /// /// Time interval after which to call the callback function. The actual minimal interval is 10-20 ms. /// Negative. /// Called not in the same thread as previously. /// API SetTimer returned 0. Unlikely. /// /// The timer will be stopped before calling the callback function. The callback function can start it again. /// If already started, this function must be called in the same thread as when started. /// public void After(int milliseconds) => _Start(true, milliseconds); /// /// Starts periodic timer. If already started, resets and changes its period. /// /// Time interval (period) of calling the callback function. The actual minimal period is 10-20 ms. /// Negative. /// Called not in the same thread as previously. /// API SetTimer returned 0. Unlikely. /// /// The callback function can stop the timer or restart with different period. /// If already started, this function must be called in the same thread as when started. /// public void Every(int milliseconds) => _Start(false, milliseconds); void _Start(bool singlePeriod, int milliseconds) { if (milliseconds < 0) throw new ArgumentOutOfRangeException(); bool isNew = _id == 0; if (!isNew) _ThreadTrap(); nint r = Api.SetTimer(default, _id, milliseconds, s_timerProc); if (r == 0) throw new Win32Exception(); Debug.Assert(isNew || r == _id); _id = r; _singlePeriod = singlePeriod; if (isNew) { _threadId = Environment.CurrentManagedThreadId; (t_timers ??= new Dictionary()).Add(_id, this); } //print.it($"Start: {_id} isNew={isNew} singlePeriod={singlePeriod} _threadId={_threadId}"); } static readonly Api.TIMERPROC s_timerProc = _TimerProc; static void _TimerProc(wnd w, int msg, nint idEvent, uint time) { //print.it(t_timers.Count, idEvent); if (!t_timers.TryGetValue(idEvent, out var t)) { //Debug_.Print($"timer id {idEvent} not in t_timers"); return; //It is possible after killing timer. // Normally API KillTimer removes WM_TIMER message from queue (tested), but in some conditions our callback can still be called several times. // For example if multiple messages are retrieved from the OS queue without dispatching each, and then all are dispatched. // Usually we can safely ignore it. But not good if the same timer id is reused for another timer. Tested on Win10: OS does not reuse ids soon. } if (t._singlePeriod) t.Stop(); try { t._action(t); } catch (Exception ex) { print.warning(ex); } //info: OS handles exceptions in timer procedure. } /// /// Stops the timer. /// /// Called not in the same thread as previously. /// /// The callback function will not be called after this. /// Later you can start the timer again (call or ). /// Don't need to call this function for single-period timers. For periodic timers it is optional; the timer stops when the thread ends. /// This function must be called in the same thread as or . /// public void Stop() { if (_id != 0) { //print.it($"Stop: {_id} _threadId={_threadId}"); _ThreadTrap(); Api.KillTimer(default, _id); //tested: KillTimer removes pending WM_TIMER messages from queue. MSDN lies. Tested on Win 10 and 7. t_timers.Remove(_id); _id = 0; } } /// /// Execute the timer action now. /// /// /// Does not change any properties. Just calls the callback function. Does not handle exceptions. /// public void Now() => _action(this); void _ThreadTrap() { bool isSameThread = _threadId == Environment.CurrentManagedThreadId; Debug.Assert(isSameThread); if (!isSameThread) throw new InvalidOperationException(nameof(timer) + " used in multiple threads."); //FUTURE: somehow allow other thread. It is often useful. } //~timer() { print.it("dtor"); } //don't call Stop() here, we are in other thread static timer _StartNew(bool singlePeriod, int milliseconds, Action timerAction, object tag = null) { var t = new timer(timerAction) { Tag = tag }; t._Start(singlePeriod, milliseconds); return t; } /// /// Creates and starts new one-time timer. /// /// New object. Usually you don't need it. /// Time interval after which to call the callback function. The actual minimal interval is 10-20 ms. /// Callback function. /// Something to pass to the callback function as . /// Negative. /// API SetTimer returned 0. Unlikely. /// /// The timer will be stopped before calling the callback function. The callback function can start it again. /// The callback function will be called in this thread. /// This thread must get/dispatch posted messages, eg call Application.Run or . The callback function is not called while this thread does not do it. /// public static timer after(int milliseconds, Action timerAction, object tag = null) => _StartNew(true, milliseconds, timerAction, tag); /// /// Creates and starts new periodic timer. /// /// New object that can be used to modify timer properties if you want to do it not in the callback function; usually don't need it. /// Time interval (period) of calling the callback function. The actual minimal period is 10-20 ms. /// Callback function. /// Something to pass to the callback function as . /// Negative. /// API SetTimer returned 0. Unlikely. /// /// The callback function can stop the timer or restart with different period. /// The callback function will be called in this thread. /// This thread must get/dispatch posted messages, eg call Application.Run or . The callback function is not called while this thread does not do it. /// public static timer every(int milliseconds, Action timerAction, object tag = null) => _StartNew(false, milliseconds, timerAction, tag); } ================================================ FILE: Au/Time/timer2.cs ================================================ namespace Au; /// /// Timer that calls callback function in other thread (thread pool) and can be used in any thread. /// /// /// Uses . /// Unlike , the thread that sets the timer does not have to retrieve/dispatch messages. /// The callback function is called in a random thread of the thread pool, therefore its code is not thread-safe (may need to lock etc). /// The actual minimal time interval/period is 10-20 ms, because the system timer period usually is 15.25 ms. /// Timer action delegates are protected from GC. /// /// /// This example sets 3 timers. /// print.it("after 500 ms")); /// timer2.every(1000, _ => print.it("every 1000 ms")); /// var t3 = new timer2(_ => print.it("after 3000 ms")); t3.After(3000); //the same as timer2.after /// 5.s(); /// ]]> /// public class timer2 { readonly Timer _tim; readonly Action _action; /// /// Sets callback function. /// public timer2(Action timerAction) { _action = timerAction; _tim = new Timer(o => { var t = o as timer2; try { t._action(t); } catch (Exception ex) { print.warning(ex); } }, this, -1, -1); //note: don't pass dueTime/period to Timer ctor. Could call callback before _tim is assigned. //s_cwt.Add(_tim, new()); //_tim = null; } //GC can collect either when explicitly stopped or when 'after' timer completes (somehow). Only after the callback returns. //~timer2() { print.it("fin"); } //static ConditionalWeakTable s_cwt=new(); //class gc { ~gc() { print.it("gc"); } } /// /// Stops the timer, and by default disposes. /// /// Just stop but don't dispose. If false (default), can't use the timer again. public void Stop(bool canReuse = false) { if (canReuse) _tim.Change(-1, -1); else _tim.Dispose(); } /// /// Starts one-time timer or changes timeout/period. /// /// Time interval after which to call the callback function. Valid values are 0 - - 2. If -1, stops without disposing. /// /// Called (unless canReuse true). /// /// Calls . /// public void After(long milliseconds) { _tim.Change(milliseconds, -1); } /// /// Starts periodic timer or changes timeout/period. /// /// Time interval (period) of calling the callback function. Valid values are 0 - - 2. /// null (default) or time interval after which to call the callback function first time. Valid values are 0 - - 2. /// /// Called (unless canReuse true). /// /// Calls . /// public void Every(long milliseconds, long? firstAfter = null) { _tim.Change(firstAfter ?? milliseconds, milliseconds); } /// /// Something to attach to this variable. /// public object Tag { get; set; } /// /// Creates and starts new one-time timer. /// /// Time interval after which to call the callback function. Valid values are 0 - - 2. If -1, stops without disposing. /// Callback function. /// Something to pass to the callback function as . /// /// /// Calls . /// public static timer2 after(long milliseconds, Action timerAction, object tag = null) { var t = new timer2(timerAction) { Tag = tag }; t.After(milliseconds); return t; } /// /// Creates and starts new periodic timer. /// /// Time interval (period) of calling the callback function. Valid values are 0 - - 2. /// Callback function. /// Something to pass to the callback function as . /// null (default) or time interval after which to call the callback function first time. Valid values are 0 - - 2. /// /// /// Calls . /// public static timer2 every(long milliseconds, Action timerAction, object tag = null, long? firstAfter = null) { var t = new timer2(timerAction) { Tag = tag }; t.Every(milliseconds, firstAfter); return t; } } ================================================ FILE: Au/Time/wait.cs ================================================ namespace Au; /// /// Contains functions to wait for a custom condition, handle, etc, or simply sleep. /// /// /// Specialized "wait for" functions are in other classes, for example . /// /// All "wait for" functions have a timeout parameter. It is the maximal time to wait, in seconds. If 0, waits indefinitely. If > 0, throws when timed out. If < 0, then stops waiting and returns default value of that type (false, etc). /// /// While waiting, most functions by default don't dispatch Windows messages, events, hooks, timers, COM/RPC, etc. For example, if used in a Window/Form/Control event handler, the window would stop responding. Use another thread, for example async/await/Task, like in the example. Or . /// /// /// keys.isScrollLock); /// print.it("ScrollLock now is toggled"); /// ]]> /// Using in a WPF window with async/await. /// { /// print.it("waiting for ScrollLock..."); /// var result = await Task.Run(() => wait.until(-10, () => keys.isScrollLock)); /// print.it(result); /// }); /// if (!b.ShowDialog()) return; /// ]]> /// public static partial class wait { /// /// Waits timeMilliseconds milliseconds. /// /// Time to wait, milliseconds. Or (-1). /// /// Calls . /// Does not process Windows messages and other events, therefore should not be used in threads with windows, timers, hooks, events or COM, unless timeMilliseconds is small. Supports APC. /// If the computer goes to sleep or hibernate during that time, the real time is the specified time + the sleep/hibernate time. /// /// Tip: the script editor replaces code like 100ms with 100.ms(); when typing. /// /// timeMilliseconds is negative and not -1 (Timeout.Infinite). /// /// /// public static void ms(this int timeMilliseconds) { SleepPrecision_.TempSet1_(timeMilliseconds); if (timeMilliseconds < 2000) { Thread.Sleep(timeMilliseconds); } else { //workaround for Thread.Sleep bug: if there are APC, returns too soon after sleep/hibernate. g1: long t = computer.tickCountWithoutSleep; Thread.Sleep(timeMilliseconds); t = timeMilliseconds - (computer.tickCountWithoutSleep - t); if (t >= 500) { timeMilliseconds = (int)t; goto g1; } } } /// /// Waits timeSeconds seconds. /// The same as , but the time is specified in seconds, not milliseconds. /// /// Time to wait, seconds. /// timeSeconds is less than 0 or greater than 2147483 ( / 1000, 24.8 days). /// /// Tip: the script editor replaces code like 100ms with 100.ms(); when typing. /// /// /// /// public static void s(this int timeSeconds) { if ((uint)timeSeconds > int.MaxValue / 1000) throw new ArgumentOutOfRangeException(); ms(timeSeconds * 1000); } /// /// Waits timeSeconds seconds. /// The same as , but the time is specified in seconds, not milliseconds. /// /// Time to wait, seconds. The smallest value is 0.001 (1 ms). /// timeSeconds is less than 0 or greater than 2147483 ( / 1000, 24.8 days). /// /// /// public static void s(this double timeSeconds) { double t = timeSeconds * 1000d; if (t > int.MaxValue || t < 0) throw new ArgumentOutOfRangeException(); ms((int)t); } //Maybe this should not be an extension method. // Code like 0.5.s() looks weird. Better 500.ms(). Rarely need non-integer time when > 1 s. // But: 1. Symmetry. 2. Easier to convert QM code, like 0.5 to 0.5.s(); not 500.ms();. /// /// Waits timeMS milliseconds. While waiting, retrieves and dispatches Windows messages and other events. /// /// Time to wait, milliseconds. Or (-1). /// /// Unlike , this function retrieves and dispatches Windows messages, calls .NET event handlers, hook procedures, timer functions, COM, etc. /// This function can be used in threads with windows. However usually there are better ways, for example timer, other thread, async/await/Task. /// If timeMS is -1, returns when receives WM_QUIT message. /// /// timeMS is negative and not -1 (Timeout.Infinite). public static unsafe void doEvents(int timeMS) { if (timeMS < -1) throw new ArgumentOutOfRangeException(); if (timeMS == 0) { Api.SleepEx(0, true); //call APC doEvents(); } else { using var mp = new MessagePump_(); for (long time = timeMS, timePrev = 0; ;) { if (timeMS > 0) { long timeNow = computer.tickCountWithoutSleep; if (timePrev > 0) time -= timeNow - timePrev; if (time <= 0) return; timePrev = timeNow; } switch (Api.MsgWaitForMultipleObjectsEx(0, null, (int)time, Api.QS_ALLINPUT, Api.MWMO_ALERTABLE | Api.MWMO_INPUTAVAILABLE)) { case 0: if (!mp.Pump() && timeMS < 0) return; break; case Api.WAIT_TIMEOUT: return; case Api.WAIT_IO_COMPLETION: break; default: throw new Win32Exception(); } } } //info: Thread.Sleep is alertable too. One reason I know is Thread.Interrupt; but with doEvents it does not work. } internal struct MessagePump_ : IDisposable { int? _quit; /// /// If received WM_QUIT, calls PostQuitMessage, unless detects that it could cause infinite loop. /// public void Dispose() { if (_quit != null) { //prevent infinite loop when eg the caller uses a loop like in doEvents(int) and ignores WM_QUIT var stack = new StackTrace(false).FrameCount; if (t_doeventsStack == 0 || stack < t_doeventsStack) { t_doeventsStack = stack; Api.PostQuitMessage(_quit.Value); } else { print.warning("duplicate WM_QUIT"); } _quit = null; } } [ThreadStatic] static int t_doeventsStack; /// /// Calls PeekMessage/TranslateMessage/DispatchMessage while there are messages. /// /// false if received WM_QUIT. public bool Pump() { while (Api.PeekMessage(out var m)) { if (m.message == Api.WM_QUIT) { _quit = (int)m.wParam; return false; } Api.TranslateMessage(m); Api.DispatchMessage(m); } return true; } /// /// Like , but can call a callback function. Used by . /// /// /// null or callback function of type: ///
- called before dispatching a message. If returns true, not called for other messages. Can modify the MSG. Can set MSG.message = 0 to prevent dispatching it. ///
Func<bool> - called after dispatching all messages. /// /// true if msgCallback returned true. public bool PumpWithCallback(Delegate msgCallback) { bool R = false; while (Api.PeekMessage(out var m)) { if (msgCallback is WPMCallback callback1) { if (callback1(ref m)) { msgCallback = null; R = true; } if (m.message == 0) continue; } if (m.message == Api.WM_QUIT) { _quit = (int)m.wParam; continue; } //now ignore, but finally repost Api.TranslateMessage(m); Api.DispatchMessage(m); } if (msgCallback is Func callback2) R = callback2(); return R; } //note: never use "dispatch only sent messages". It's dangerous and not useful. // If thread has windows, hangs if we don't get posted messages. // Else PeekMessage usually does not harm. //note: with PeekMessage don't use |Api.PM_QS_SENDMESSAGE when don't need posted messages. // Then setwineventhook hook does not work. Although setwindowshookex hook works. COM RPC not tested. } /// /// Retrieves and dispatches events and Windows messages from the message queue of this thread. /// /// false if received WM_QUIT message. /// /// Similar to , but more lightweight. Uses API functions PeekMessage, TranslateMessage and DispatchMessage. /// public static bool doEvents() { using var mp = new MessagePump_(); return mp.Pump(); } /// /// Calls and . /// internal static unsafe void doEventsPrecise_(int timeMS) { SleepPrecision_.TempSet1_(timeMS); doEvents(timeMS); } /// /// Temporarily changes the time resolution/precision of Thread.Sleep and some other functions. /// /// /// Uses API timeBeginPeriod, which requests a time resolution for various system timers and wait functions. Actually it is the system thread scheduling timer period. /// Normal resolution on Windows 7-10 is 15.625 ms. It means that, for example, Thread.Sleep(1); sleeps not 1 but 1-15 ms. If you set resolution 1, it sleeps 1-2 ms. /// The new resolution is revoked (timeEndPeriod) when disposing the SleepPrecision_ variable or when this process ends. See example. See also . /// The resolution is applied to all threads and processes. Other applications can change it too. For example, often web browsers temporarily set resolution 1 ms when opening a web page. /// The system uses the smallest period (best resolution) that currently is set by any application. You cannot make it bigger than current value. /// It is not recommended to keep small period (high resolution) for a long time. It can be bad for power saving. /// Don't need this for etc and functions that use them. They call when the sleep time is 1-89 ms. /// This does not change the minimal period of timers. /// /// /// /// internal sealed class SleepPrecision_ : IDisposable { //info: this class could be public, but probably not useful. wait.ms automatically sets 1 ms period if need. int _period; /// /// Calls API timeBeginPeriod. /// /// /// New system timer period, milliseconds. /// Should be 1. Other values may stuck and later cannot be made smaller due to bugs in OS or some applications; this bug would impact many functions of this library. /// /// periodMS <= 0. public SleepPrecision_(int periodMS) { if (periodMS <= 0) throw new ArgumentOutOfRangeException(); if (Api.timeBeginPeriod((uint)periodMS) != 0) return; //print.it("set"); _period = periodMS; //Bug in OS or drivers or some apps: // On my main PC often something briefly sets 0.5 ms resolution. // If at that time this process already has set a resolution of more than 1 ms, then after that time this process cannot change resolution. // It means that if this app eg has set 10 ms resolution, then wait.ms(1) will sleep 10 ms and not the normal 1-2 ms. // Known workaround (but don't use, sometimes does not work, eg cannot end period that was set by another process): // timeBeginPeriod(periodMS); // var r=(int)Current; if(r>periodMS) { timeEndPeriod(periodMS); timeEndPeriod(r); timeBeginPeriod(r); timeBeginPeriod(periodMS); } } /// /// Calls API timeEndPeriod. /// public void Dispose() { _Dispose(); GC.SuppressFinalize(this); } void _Dispose() { if (_period == 0) return; //print.it("revoke"); Api.timeEndPeriod((uint)_period); _period = 0; } /// ~SleepPrecision_() { _Dispose(); } /// /// Gets current actual system time resolution (period). /// /// The return value usually is between 0.5 and 15.625 milliseconds. Returns 0 if failed. public static float Current { get { if (0 != Api.NtQueryTimerResolution(out _, out _, out var t)) return 0f; return (float)t / 10000; } } /// /// Temporarily sets the system wait precision to 1 ms. It will be revoked after the specified time or when this process ends. /// If already set, just updates the revoking time. /// /// Revoke after this time, milliseconds. /// /// /// public static void TempSet1(int endAfterMS = 1111) { lock ("2KgpjPxRck+ouUuRC4uBYg") { s_TS1_EndTime = computer.tickCountWithoutSleep + endAfterMS; if (s_TS1_Obj == null) { s_TS1_Obj = new SleepPrecision_(1); //info: instead could call the API directly, but may need to auto-revoke using the finalizer ThreadPool.QueueUserWorkItem(endAfterMS2 => { Thread.Sleep((int)endAfterMS2); //note: don't use captured variables. It creates new garbage all the time. for (; ; ) { int t; lock ("2KgpjPxRck+ouUuRC4uBYg") { t = (int)(s_TS1_EndTime - computer.tickCountWithoutSleep); if (t <= 0) { s_TS1_Obj.Dispose(); s_TS1_Obj = null; break; } } Thread.Sleep(t); } }, endAfterMS); //performance (old info): single QueueUserWorkItem adds 3 threads, >=2 adds 5. But Thread.Start is too slow etc. //QueueUserWorkItem speed first time is similar to Thread.Start, then ~8. //Task.Run and Task.Delay are much much slower first time. Single Delay adds 5 threads. } } //tested: Task Manager shows 0% CPU. If we set/revoke period for each Sleep(1) in loop, shows ~0.5% CPU. } static SleepPrecision_ s_TS1_Obj; static long s_TS1_EndTime; //never mind: finalizer is not called on process exit. // Not a problem, because OS clears our set value (tested). Or we could use process.thisProcessExit event. /// /// Calls TempSet1 if sleepTimeMS is 1-89. /// /// milliseconds of the caller "sleep" function. internal static void TempSet1_(int sleepTimeMS) { if (sleepTimeMS is < 90 and > 0) TempSet1(1111); } } } ================================================ FILE: Au/Time/wait_for.cs ================================================ namespace Au { public static partial class wait { /// /// Waits for a user-defined condition. Until the callback function returns a value other than default(T), for example true. /// /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// Callback function (eg lambda). It is called repeatedly, until returns a value other than default(T). Default period is 10, and can be changed like wait.until(new(5) { Timeout = 100 }, ...). /// Returns the value returned by the callback function. On timeout returns default(T) if timeout is negative; else exception. /// /// See . public static T until(Seconds timeout, Func condition) { var loop = new WaitLoop(timeout); for (; ; ) { T r = condition(); if (!EqualityComparer.Default.Equals(r, default)) return r; if (!loop.Sleep()) return r; } } /// /// Obsolete. Use wait.until. /// Waits for a user-defined condition. Until the callback function returns a value other than default(T), for example true. /// /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// Callback function (eg lambda). It is called repeatedly, until returns a value other than default(T). The calling period depends on options. /// Options. If null, uses opt.wait. /// Returns the value returned by the callback function. On timeout returns default(T) if secondsTimeout is negative; else exception. #if DEBUG [Obsolete] #endif [EditorBrowsable(EditorBrowsableState.Never)] public static T forCondition(double secondsTimeout, Func condition, OWait options = null) { #pragma warning disable CS0612 // Type or member is obsolete var loop = new WaitLoop(secondsTimeout, options); #pragma warning restore CS0612 // Type or member is obsolete for (; ; ) { T r = condition(); if (!EqualityComparer.Default.Equals(r, default)) return r; if (!loop.Sleep()) return r; } } /// /// Calls callback function action. If it throws an exception, waits/retries until it does not throw exceptions or until timeout. /// /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// Callback function (eg lambda). It is called repeatedly, until does not throw an exception. Default period is 10, and can be changed like wait.retry(new(5) { Timeout = 100 }, ...). /// Called on exception. Return true to handle the exception (and wait/retry). Return false to not handle the exception. If null (default), handles all exceptions. /// Returns true when action succeeded. On timeout returns false if timeout is negative; else exception. /// /// /// This example uses both wait.retry overloads - with parameter Func func and with parameter Action action. /// File.ReadAllText(file)); /// s = s.Upper(); /// wait.retry(5, () => { File.WriteAllText(file, s); }); /// print.it("ok"); /// ]]> /// Set the retry period. /// { File.WriteAllText(file, s); }); /// ]]> /// Handle only exceptions of some types. /// { File.WriteAllText(file, s); }, e => e is IOException); /// ]]> /// public static bool retry(Seconds timeout, Action action, Func catchWhen = null) { var loop = new WaitLoop(timeout); for (; ; ) { try { action(); return true; } catch (Exception e) when (catchWhen?.Invoke(e) != false) { } if (!loop.Sleep()) return false; } } /// /// Calls callback function func and returns its result. If it throws an exception, waits/retries until it does not throw exceptions or until timeout. /// /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// Callback function (eg lambda). It is called repeatedly, until does not throw an exception. Default period is 10, and can be changed like wait.retry(new(5) { Timeout = 100 }, ...). /// Called on exception. Return true to handle the exception (and wait/retry). Return false to not handle the exception. If null (default), handles all exceptions. /// Returns the value returned by the callback function. On timeout returns default(T) if timeout is negative; else exception. /// public static T retry(Seconds timeout, Func func, Func catchWhen = null) { var loop = new WaitLoop(timeout); for (; ; ) { try { return func(); } catch (Exception e) when (catchWhen?.Invoke(e) != false) { } if (!loop.Sleep()) return default; } } //rejected. This overload could be useful when need exact try count and period. But probably rarely useful when we have the overloads with Seconds. Maybe in the future, if really useful. Also need Func overload, CancellationToken, max period, doevents option, etc. //public static bool retry(int tryCount, int periodMS, Action action, Func catchWhen = null) { // ArgumentOutOfRangeException.ThrowIfEqual(tryCount, 0); // ArgumentOutOfRangeException.ThrowIfNegative(periodMS); // bool noThrow = tryCount < 0; if (noThrow) tryCount = -tryCount; // while (tryCount-- > 0) { // try { action(); return true; } // catch (Exception e) when ((tryCount > 0 || noThrow) && catchWhen?.Invoke(e) != false) { } // if (tryCount > 0 && periodMS > 0) Thread.Sleep(periodMS); // } // return false; //} /// /// Waits for a kernel object (event, mutex, etc). /// /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// /// One or more handles of kernel objects. Max 63. /// /// Returns 1-based index of the first signaled handle. Negative if abandoned mutex. /// On timeout returns 0 if timeout is negative; else exception. /// /// timeout time has expired (if > 0). /// Failed. For example a handle is invalid. /// /// Uses API WaitForMultipleObjectsEx or MsgWaitForMultipleObjectsEx. Alertable. /// Does not use , and . /// public static int forHandle(Seconds timeout, WHFlags flags, params ReadOnlySpan handles) { return WaitS_(timeout, flags, handles); } /// /// Waits for handles, or/and msgCallback returning true, or/and stopVar becoming true. /// Calls . /// /// ///
• 0 if timeout (if timeout < 0), ///
1-handles.Length if signaled, ///
-(1-handles.Length) if abandoned mutex, ///
1+handles.Length if msgCallback returned true, ///
2+handles.Length if stop became true. ///
internal static int WaitS_(Seconds timeout, WHFlags flags, ReadOnlySpan handles = default, Delegate msgCallback = null, WaitVariable_ stopVar = null) { if (timeout.Period != null || timeout.MaxPeriod != null) print.warning("This wait function does not use Seconds.Period/MaxPeriod."); if (flags.Has(WHFlags.DoEvents)) { if (timeout.DoEvents != null) print.warning("This wait function does not use Seconds.DoEvents. It always works like if it is true."); } else if (timeout.DoEvents == true) flags |= WHFlags.DoEvents; long timeMS = _TimeoutS2MS(timeout, out bool canThrow); int r = Wait_(timeMS, flags, handles, msgCallback, stopVar, timeout.Cancel); if (r < 0) throw new AuException(0); if (r == Api.WAIT_TIMEOUT) { if (canThrow) throw new TimeoutException(); return 0; } r++; if (r > Api.WAIT_ABANDONED_0) r = -r; return r; } static long _TimeoutS2MS(Seconds timeout, out bool canThrow) { canThrow = false; var t = timeout.Time; if (t == 0) return -1; if (t < 0) t = -t; else canThrow = true; return checked((long)(t * 1000d)); } /// /// Waits for handles, or/and msgCallback returning true, or/and stopVar becoming true. Or just sleeps, if handles etc are null/empty. /// If flag DoEvents, dispatches received messages etc. /// Calls API WaitForMultipleObjectsEx or MsgWaitForMultipleObjectsEx with QS_ALLINPUT. Alertable. /// /// /// Called when dispatching messages. If returns true, stops waiting and returns handles.Length. /// If it is WPMCallback, calls it before dispatching a posted message. /// If it is Func{bool}, calls it after dispatching one or more messages. /// /// When becomes true, stops waiting and returns handles.Length + 1. /// /// When a handle becomes signaled, returns its 0-based index. If abandoned mutex, returns 0-based index + WAIT_ABANDONED_0 (0x80). /// If timeMS>0, waits max timeMS and on timeout returns WAIT_TIMEOUT. /// If failed, returns -1. Supports . /// internal static unsafe int Wait_(long timeMS, WHFlags flags, ReadOnlySpan handles = default, Delegate msgCallback = null, WaitVariable_ stopVar = null, CancellationToken cancel = default) { //rejected. Don't complicate code to implement rarely used features. //if (cancel.CanBeCanceled) { // var ch = cancel.WaitHandle.SafeWaitHandle.DangerousGetHandle(); // handles = handles == null ? new[] { ch } : handles.InsertAt(-1, ch); //} bool doEvents = flags.Has(WHFlags.DoEvents); Debug.Assert(doEvents || (msgCallback == null && stopVar == null)); int nHandles = handles.Length; bool all = flags.Has(WHFlags.All) && nHandles > 1; using var mp = new MessagePump_(); fixed (IntPtr* ha = handles) { for (long timePrev = 0; ;) { cancel.ThrowIfCancellationRequested(); if (stopVar != null && stopVar.waitVar) return nHandles + 1; int timeSlice = all && doEvents ? 50 : cancel.CanBeCanceled ? 250 : 5000; if (timeMS > 0) { long timeNow = computer.tickCountWithoutSleep; if (timePrev > 0) timeMS -= timeNow - timePrev; if (timeMS <= 0) return Api.WAIT_TIMEOUT; if (timeSlice > timeMS) timeSlice = (int)timeMS; timePrev = timeNow; } else if (timeMS == 0) timeSlice = 0; int k; if (doEvents && !all) { k = Api.MsgWaitForMultipleObjectsEx(nHandles, ha, timeSlice, Api.QS_ALLINPUT, Api.MWMO_ALERTABLE | Api.MWMO_INPUTAVAILABLE); if (k == nHandles) { //message, COM, hook, etc if (mp.PumpWithCallback(msgCallback)) return nHandles; continue; } } else { if (nHandles > 0) k = Api.WaitForMultipleObjectsEx(nHandles, ha, all, timeSlice, true); else { k = Api.SleepEx(timeSlice, true); if (k == 0) k = Api.WAIT_TIMEOUT; } if (doEvents) if (mp.PumpWithCallback(msgCallback)) return nHandles; } if (k is not (Api.WAIT_TIMEOUT or Api.WAIT_IO_COMPLETION)) return k; //signaled handle, abandoned mutex, WAIT_FAILED (-1) } } } /// /// Waits for a posted message received by this thread. /// /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// Callback function that returns true to stop waiting. More info in Remarks. /// Returns true. On timeout returns false if timeout is negative; else exception. /// timeout time has expired (if > 0). /// /// While waiting, dispatches Windows messages etc, like . Before dispatching a posted message, calls the callback function. Stops waiting when it returns true. Does not dispatch the message if the function sets the message field = 0. /// Does not use , , and . /// /// /// { print.it("timer"); }); /// wait.forPostedMessage(5, (ref MSG m) => { print.it(m); return m.message == 0x113; }); //WM_TIMER /// print.it("finished"); /// ]]> /// public static bool forPostedMessage(Seconds timeout, WPMCallback callback) { return 1 == WaitS_(timeout, WHFlags.DoEvents, msgCallback: callback); } /// /// Waits for a condition to be changed while processing messages or other events received by this thread. /// /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// Callback function that returns true to stop waiting. More info in Remarks. /// Returns true. On timeout returns false if timeout is negative; else exception. /// timeout time has expired (if > 0). /// /// While waiting, dispatches Windows messages etc, like . After dispatching one or more messages or other events (posted messages, messages sent by other threads, hooks, COM, APC, etc), calls the callback function. Stops waiting when it returns true. /// Similar to . Differences: 1. Always dispatches messages etc. 2. Does not call the callback function when there are no messages etc. 3. Does not use , , and . /// /// /// { print.it("timer"); stop = true; }); /// wait.doEventsUntil(5, () => stop); /// print.it(stop); /// ]]> /// public static bool doEventsUntil(Seconds timeout, Func condition) { return 1 == WaitS_(timeout, WHFlags.DoEvents, msgCallback: condition); } /// /// Obsolete. Use wait.doEventsUntil. /// /// #if DEBUG [Obsolete] //just renamed #endif [EditorBrowsable(EditorBrowsableState.Never)] public static bool forMessagesAndCondition(double secondsTimeout, Func condition) => doEventsUntil(secondsTimeout, condition); //rejected. Rarely used; type-limited. Let use wait.until. //public static bool forVariable(Seconds timeout, in bool variable, OWait options = null) { } //FUTURE: add misc wait functions implemented using WindowsHook and WinEventHook. } } namespace Au.Types { /// /// Flags for /// [Flags] public enum WHFlags { /// /// Wait until all handles are signaled. /// All = 1, /// /// While waiting, dispatch Windows messages, events, hooks etc. Like . /// DoEvents = 2, } /// /// Delegate type for . /// /// API MSG. public delegate bool WPMCallback(ref MSG m); /// /// Used with Wait_ etc instead of ref bool. /// internal class WaitVariable_ { public bool waitVar; } } //CONSIDER: in QM2 these functions are created: // WaitForFocus, WaitWhileWindowBusy, // WaitForFileReady, WaitForChangeInFolder, // ChromeWait, FirefoxWait // WaitForTime, // these are in System: WaitIdle, WaitForThreads, //CONSIDER: WaitForFocusChanged // Eg when showing Open/SaveAs dialog, the file Edit control receives focus after 200 ms. Sending text to it works anyway, but the script fails if then it clicks OK not with keys (eg with elm). ================================================ FILE: Au/Triggers/Trigger.cs ================================================ namespace Au.Triggers; /// /// Base of classes of all action trigger types. /// public abstract class ActionTrigger { internal ActionTrigger next; //linked list when eg same hotkey is used in multiple scopes internal readonly ActionTriggers triggers; internal readonly TOptions options; //Triggers.Options readonly TriggerFunc[] _funcAfter, _funcBefore; //Triggers.FuncOf. _funcAfter used by all triggers; _funcBefore - like scope. internal readonly Delegate action; /// /// Triggers.Of.WindowX. Used by hotkey, autotext and mouse triggers. /// public TriggerScope Scope { get; } /// public string SourceFile { get; } /// public int SourceLine { get; } internal ActionTrigger(ActionTriggers triggers, Delegate action, bool usesWindowScope, (string, int) source) { this.SourceFile = source.Item1 ?? throw new ArgumentNullException(); this.SourceLine = source.Item2; this.action = action; this.triggers = triggers; var to = triggers.options_; options = to.Current; EnabledAlways = to.EnabledAlways; if (usesWindowScope) Scope = triggers.scopes_.Current_; var tf = triggers.funcs_; _funcBefore = _Func(tf.commonBefore, tf.nextBefore); tf.nextBefore = null; _funcAfter = _Func(tf.nextAfter, tf.commonAfter); tf.nextAfter = null; TriggerFunc[] _Func(TFunc f1, TFunc f2) { var f3 = f1 + f2; if (f3 == null) return null; var a1 = f3.GetInvocationList(); var r1 = new TriggerFunc[a1.Length]; for (int i = 0; i < a1.Length; i++) { var f4 = a1[i] as TFunc; if (!tf.perfDict.TryGetValue(f4, out var fs)) tf.perfDict[f4] = fs = new TriggerFunc { f = f4 }; r1[i] = fs; } return r1; } } internal void DictAdd_(Dictionary d, TKey key) { if (!d.TryGetValue(key, out var o)) d.Add(key, this); else { //append to the linked list while (o.next != null) o = o.next; o.next = this; } } /// /// Called through in action thread. /// Possibly runs later. /// internal abstract void Run_(TriggerArgs args); /// /// Makes simpler to implement . /// private protected void RunT_(T args) => (action as Action)(args); /// /// Returns a trigger type string, like "Hotkey", "Mouse", "Window.ActiveNew". /// public abstract string TypeString { get; } /// /// Returns a string containing trigger parameters. /// public abstract string ParamsString { get; } /// /// Returns TypeString + " " + ParamsString. /// public override string ToString() => TypeString + " " + ParamsString; internal bool MatchScopeWindowAndFunc_(TriggerHookContext thc) { try { for (int i = 0; i < 3; i++) { if (i == 1) { if (Scope != null) { thc.PerfStart(); bool ok = Scope.Match(thc.Window, thc); thc.PerfEnd(false, ref Scope.perfTime); if (!ok) return false; } } else { var af = i == 0 ? _funcBefore : _funcAfter; if (af != null) { foreach (var v in af) { thc.PerfStart(); bool ok = v.f(thc.args); thc.PerfEnd(true, ref v.perfTime); if (!ok) return false; } } } } } catch (Exception ex) { print.it(ex); return false; } return true; //never mind: when same scope used several times (probably with different functions), // should compare it once, and don't call 'before' functions again if did not match. Rare. } internal bool CallFunc_(TriggerArgs args) { #if true if (_funcAfter != null) { try { foreach (var v in _funcAfter) { var t1 = perf.ms; bool ok = v.f(args); var td = perf.ms - t1; if (td > 200) print.warning($"Too slow Triggers.FuncOf function of a window trigger. Should be < 10 ms, now {td} ms. Task name: {script.name}.", -1); if (!ok) return false; } } catch (Exception ex) { print.it(ex); return false; } } #else for(int i = 0; i < 2; i++) { var af = i == 0 ? _funcBefore : _funcAfter; if(af != null) { foreach(var v in af) { bool ok = v.f(args); if(!ok) return false; } } } #endif return true; //TODO3: measure time more intelligently, like in MatchScope, but maybe give more time. } internal bool HasFunc_ => _funcBefore != null || _funcAfter != null; //probably not useful. Or also need a property for eg HotkeyTriggers in derived classes. ///// ///// The instance to which this trigger belongs. ///// //public ActionTriggers Triggers => triggers; /// /// Gets or sets whether this trigger is disabled. /// Does not depend on , , . /// public bool Disabled { get; set; } /// /// Returns true if ; also if or , unless . /// public bool DisabledThisOrAll => Disabled || (!EnabledAlways && (triggers.Disabled | ActionTriggers.DisabledEverywhere)); /// /// Gets or sets whether this trigger ignores and . /// /// /// When adding the trigger, this property is set to the value of at that time. /// public bool EnabledAlways { get; set; } /// /// Starts the action like when its trigger is activated. /// /// /// Called before or after . /// /// This function must be called while the main triggers thread is in , for example from another trigger action. It is asynchronous (does not wait). /// If called from a trigger action (hotkey etc), make sure this action runs in another thread or can be queued. Else both actions cannot run simultaneously. /// public void RunAction(TriggerArgs args) { triggers.ThrowIfNotRunning_(); if (triggers.IsMainThread) { triggers.RunAction_(this, args); } else { triggers.SendMsg_(false, () => triggers.RunAction_(this, args)); } } } /// /// Base of trigger action argument classes of all trigger types. /// public abstract class TriggerArgs { /// /// Gets the trigger as (the base class of all trigger type classes). /// public abstract ActionTrigger TriggerBase { get; } /// /// Disables the trigger. Enables later when the toolbar is closed. /// Use to implement single-instance toolbars. /// public void DisableTriggerUntilClosed(toolbar t) { TriggerBase.Disabled = true; t.Closed += () => TriggerBase.Disabled = false; } } /// /// Allows to specify working windows for multiple triggers of these types: hotkey, autotext, mouse. /// /// /// Note: the Triggers in examples is a field or property like readonly ActionTriggers Triggers = new();. /// print.it("this trigger works with all windows"); /// Triggers.Of.Window("* Notepad"); //specifies a working window for triggers added afterwards /// Triggers.Hotkey["Ctrl+F11"] = o => print.it("this trigger works only when a Notepad window is active"); /// Triggers.Hotkey["Ctrl+F12"] = o => print.it("this trigger works only when a Notepad window is active"); /// var chrome = Triggers.Of.Window("* Chrome"); //specifies another working window for triggers added afterwards /// Triggers.Hotkey["Ctrl+F11"] = o => print.it("this trigger works only when a Chrome window is active"); /// Triggers.Hotkey["Ctrl+F12"] = o => print.it("this trigger works only when a Chrome window is active"); /// Triggers.Of.AllWindows(); //let triggers added afterwards work with all windows /// Triggers.Mouse[TMEdge.RightInTop25] = o => print.it("this trigger works with all windows"); /// Triggers.Of.Again(chrome); //sets a previously specified working window for triggers added afterwards /// Triggers.Mouse[TMEdge.RightInBottom25] = o => print.it("this trigger works only when a Chrome window is active"); /// Triggers.Mouse[TMMove.DownUp] = o => print.it("this trigger works only when a Chrome window is active"); /// Triggers.Mouse[TMClick.Middle] = o => print.it("this trigger works only when the mouse is in a Chrome window"); /// Triggers.Mouse[TMWheel.Forward] = o => print.it("this trigger works only when the mouse is in a Chrome window"); /// Triggers.Run(); /// ]]> /// public class TriggerScopes { internal TriggerScopes() { } internal TriggerScope Current_ { get; private set; } //rejected. More confusing than useful. ///// ///// Sets the scope that was active before the last call to any "set scope" function. ///// //public void PreviousScope() => Current = _previous; //internal TriggerScope Current_ { get => _current; private set { _previous = _current; _current = value; } } //TriggerScope _current, _previous; /// /// Sets scope "all windows" again. Hotkey, autotext and mouse triggers added afterwards will work with all windows. /// /// /// Example in class help. /// public void AllWindows() => Current_ = null; /// /// Sets (reuses) a previously specified scope. /// /// /// Example in class help. /// /// The return value of function , , or . public void Again(TriggerScope scope) => Current_ = scope; /// /// Sets scope "only this window". Hotkey, autotext and mouse triggers added afterwards will work only when the specified window is active. /// /// Returns an object that can be later passed to to reuse this scope. /// /// public TriggerScope Window( [ParamString(PSFormat.Wildex)] string name = null, [ParamString(PSFormat.Wildex)] string cn = null, [ParamString(PSFormat.Wildex)] WOwner of = default, Func also = null, WContains contains = default) => _Window(false, name, cn, of, also, contains); /// /// Sets scope "not this window". Hotkey, autotext and mouse triggers added afterwards will not work when the specified window is active. /// /// public TriggerScope NotWindow( [ParamString(PSFormat.Wildex)] string name = null, [ParamString(PSFormat.Wildex)] string cn = null, [ParamString(PSFormat.Wildex)] WOwner of = default, Func also = null, WContains contains = default) => _Window(true, name, cn, of, also, contains); TriggerScope _Window(bool not, string name, string cn, WOwner of, Func also, WContains contains) => _Add(not, new wndFinder(name, cn, of, 0, also, contains)); /// /// Sets scope "only this window". Hotkey, autotext and mouse triggers added afterwards will work only when the specified window is active. /// /// Returns an object that can be later passed to to reuse this scope. public TriggerScope Window(wndFinder f) => _Add(false, f); /// /// Sets scope "not this window". Hotkey, autotext and mouse triggers added afterwards will not work when the specified window is active. /// /// Returns an object that can be later passed to to reuse this scope. public TriggerScope NotWindow(wndFinder f) => _Add(true, f); //rejected. May be used incorrectly. Rare. When really need, can use the 'also' parameter. ///// ///// Sets scope "only this window". Hotkey, autotext and mouse triggers added afterwards will work only when the specified window is active. ///// ///// Returns an object that can be later passed to to reuse this scope. ///// Invalid window handle. //public TriggerScope Window(wnd w) // => _Add(false, w); ///// ///// Sets scope "not this window". Hotkey, autotext and mouse triggers added afterwards will not work when the specified window is active. ///// ///// Returns an object that can be later passed to to reuse this scope. ///// Invalid window handle. //public TriggerScope NotWindow(wnd w) // => _Add(true, w); /// /// Sets scope "only these windows". Hotkey, autotext and mouse triggers added afterwards will work only when one of the specified windows is active. /// /// Returns an object that can be later passed to to reuse this scope. /// Specifies windows, like new("Window1"), new("Window2"). public TriggerScope Windows(params wndFinder[] any) => _Add(false, any); /// /// Sets scope "not these windows". Hotkey, autotext and mouse triggers added afterwards will not work when one of the specified windows is active. /// /// Returns an object that can be later passed to to reuse this scope. /// Specifies windows, like new("Window1"), new("Window2"). public TriggerScope NotWindows(params wndFinder[] any) => _Add(true, any); TriggerScope _Add(bool not, wndFinder f) { Not_.Null(f); Used_ = true; return Current_ = new TriggerScope(f, not); } TriggerScope _Add(bool not, wndFinder[] a) { if (a.Length == 1) return _Add(not, a[0]); foreach (var v in a) if (v == null) throw new ArgumentNullException(); Used_ = true; return Current_ = new TriggerScope(a, not); } internal bool Used_ { get; private set; } } /// /// A trigger scope returned by functions like and used with . /// /// See . public class TriggerScope { internal readonly object o; //wndFinder, wndFinder[] internal readonly bool not; internal int perfTime; internal TriggerScope(object o, bool not) { this.o = o; this.not = not; } /// /// Returns true if the window matches this scope. /// public bool Match(wnd w, WFCache cache) { bool yes = false; if (!w.Is0) { switch (o) { case wndFinder f: yes = f.IsMatch(w, cache); break; case wndFinder[] a: foreach (var v in a) { if (yes = v.IsMatch(w, cache)) break; } break; } } return yes ^ not; } } /// /// Allows to define custom scopes/contexts/conditions for triggers. /// /// /// Similar to (code like Triggers.Of.Window(...);), but allows to define any scope/condition/etc, not just the active window. /// /// To define a scope, you create a callback function (CF) that checks some conditions and returns true to allow the trigger action to run or false to not allow. Assign the CF to some property of this class and then add the trigger, like in the examples below. The CF will be assigned to the trigger and called when need. /// /// You may ask: why to use CF when the trigger action (TA) can do the same? /// 1. CF runs synchronously; if it returns false, the trigger key or mouse button message is passed to other triggers, hooks and apps. TA cannot do it reliably; it runs asynchronously, and the message is already stealed from other apps/triggers/hooks. /// 2. CF is faster to call. It is simply called in the same thread that processes trigger messages. TA usually runs in another thread. /// 3. A CF can be assigned to multiple triggers with a single line of code. Don't need to add the same code in all trigger actions. /// /// A trigger can have up to 4 CF delegates and a window scope (Triggers.Of...). They are called in this order: CF assigned through , , window scope, , . The NextX properties assign the CF to the next single trigger. The FollowingX properties assign the CF to all following triggers until you assign another CF or null. If several are assigned, the trigger action runs only if all CF return true and the window scope matches. The XBeforeWindow properties are used only with hotkey, autotext and mouse triggers. /// /// All CF must be as fast as possible. Slow CF can make triggers slower (or even all keyboard/mouse input); also may cause warnings and trigger failures. A big problem is the low-level hooks timeout that Windows applies to trigger hooks; see . A related problem - slow JIT and loading of assemblies, which can make the CF too slow the first time; in some rare cases may even need to preload assemblies or pre-JIT functions to avoid the timeout warning. /// /// In CF never use functions that generate keyboard or mouse events or activate windows. /// /// /// Note: the Triggers in examples is a field or property like readonly ActionTriggers Triggers = new();. /// keys.isCapsLock; //o => keys.isCapsLock is the callback function (lambda) /// Triggers.Hotkey["Ctrl+K"] = o => print.it("action: Ctrl+K while CapsLock is on"); /// Triggers.FuncOf.NextTrigger = o => { var v = o as HotkeyTriggerArgs; print.it($"func: mod={v.Mod}"); return mouse.isPressed(MButtons.Left); }; /// Triggers.Hotkey["Ctrl+Shift?+B"] = o => print.it("action: mouse left button + Ctrl+B or Ctrl+Shift+B"); /// /// //examples of assigning a CF to multiple triggers /// Triggers.FuncOf.FollowingTriggers = o => { var v = o as HotkeyTriggerArgs; print.it("func", v); return true; }; /// Triggers.Hotkey["Ctrl+F8"] = o => print.it("action: " + o); /// Triggers.Hotkey["Ctrl+F9"] = o => print.it("action: " + o); /// Triggers.FuncOf.FollowingTriggers = null; //stop assigning the CF to triggers added afterwards /// /// //sometimes all work can be done in CF and you don't need the trigger action /// Triggers.FuncOf.NextTrigger = o => { var v = o as HotkeyTriggerArgs; print.it("func: " + v); return true; }; /// Triggers.Hotkey["Ctrl+F12"] = null; /// /// Triggers.Run(); /// ]]> /// public class TriggerFuncs { internal TriggerFuncs() { } internal Dictionary perfDict = new Dictionary(); //internal bool Used_ { get; private set; } internal TFunc nextAfter, nextBefore, commonAfter, commonBefore; /// /// Sets callback function for the next added trigger. /// Called after evaluating the window scope (if any). /// public TFunc NextTrigger { get => nextAfter; set => nextAfter = _Func(value); } /// /// Sets callback function for the next added trigger (except window triggers). /// Called before evaluating the window scope (if any). /// public TFunc NextTriggerBeforeWindow { get => nextBefore; set => nextBefore = _Func(value); } /// /// Sets callback function for triggers added afterwards. /// Called after evaluating the window scope (if any). /// public TFunc FollowingTriggers { get => commonAfter; set => commonAfter = _Func(value); } /// /// Sets callback function for triggers added afterwards (except window triggers). /// Called before evaluating the window scope (if any). /// public TFunc FollowingTriggersBeforeWindow { get => commonBefore; set => commonBefore = _Func(value); } TFunc _Func(TFunc f) { //if(f != null) Used = true; return f; } /// /// Clears all properties (sets = null). /// public void Reset() { nextAfter = null; nextBefore = null; commonAfter = null; commonBefore = null; } } class TriggerFunc { internal TFunc f; internal int perfTime; } /// /// Type of functions used with class to define custom scope for triggers. /// /// Trigger action arguments. Example: . /// Return true to run the trigger action, or false to not run. public delegate bool TFunc(TriggerArgs args); ================================================ FILE: Au/Triggers/Triggers.cs ================================================ namespace Au.Triggers; /// /// The main class of action triggers. /// /// /// This class manages action triggers. Action triggers are used to call functions (aka trigger actions) in a running script in response to events such as hotkey, typed text, mouse action, activated window. To launch scripts are used other ways: manually, at startup, command line, , output link. /// /// If your script has a variable, field or property like ActionTriggers Triggers = new();, through it you can access all trigger types (hotkey, window, etc) and add triggers to them. /// /// Code syntax to add an action trigger: /// Triggers.TriggerType[parameters] = action; /// Examples: /// print.it(o); /// Triggers.Hotkey["Ctrl+Shift+K"] = o => { /// print.it("This is a trigger action (lambda function)."); /// print.it($"It runs when you press {o}."); /// }; /// Triggers.Run(); /// ]]> /// /// Also you can set options (), window scopes () and custom scopes () for triggers added afterwards. /// /// Finally call or . It runs all the time and launches trigger actions (functions) when need. Actions run in other thread(s) by default. /// /// To quickly restart the script when editing, click the Run button. /// /// Avoid multiple scripts with triggers. Each running instance uses some CPU. All triggers should be in single script, if possible. It's OK to run additional scripts temporarily, for example to test new triggers without restarting the main script. From trigger actions you can call to run other scripts in new process; see example. /// /// Trigger actions may not inherit options that are set before adding triggers. The example shows how to correctly set options for multiple actions. Also you can set them in action code. Next action running in the same thread will not inherit options set by previous action. /// /// /// This is a single script with many action triggers. /// print.it(o); //it means: execute code "o => print.it(o)" when I press Ctrl+K /// hk["Ctrl+Shift+F11"] = o => { /// print.it(o); /// var w1 = wnd.findOrRun("* Notepad", run: () => run.it(folders.System + "notepad.exe")); /// keys.sendt("text"); /// w1.Close(); /// }; /// hk["Win+Alt+K"] = o => script.run("Example.cs"); //run another script in new process /// /// //triggers that work only with some windows /// /// Triggers.Of.Window("* Chrome", "Chrome_WidgetWin_1"); //let the following triggers work only when a Chrome window is active /// hk["Ctrl+F5"] = o => print.it(o, o.Window); /// hk["Ctrl+F6"] = o => print.it(o, o.Window); /// /// var notepad = Triggers.Of.Window("* Notepad"); //let the following triggers work only when a Notepad window is active /// hk["Ctrl+F5"] = o => print.it(o, o.Window); /// hk["Ctrl+F6"] = o => print.it(o, o.Window); /// /// Triggers.Of.AllWindows(); //let the following triggers work with all windows /// /// //mouse triggers /// /// mouse[TMClick.Right, "Ctrl+Shift", TMFlags.ButtonModUp] = o => print.it(o); /// mouse[TMEdge.RightInCenter50] = o => { print.it(o); dialog.show("Bang!", x: Coord.Max); }; /// mouse[TMMove.LeftRightInCenter50] = o => wnd.switchActiveWindow(); /// /// Triggers.FuncOf.NextTrigger = o => keys.isScrollLock; //example of a custom scope (aka context, condition) /// mouse[TMWheel.Forward] = o => print.it($"{o} while ScrollLock is on"); /// /// Triggers.Of.Again(notepad); //let the following triggers work only when a Notepad window is active /// mouse[TMMove.LeftRightInBottom25] = o => { print.it(o); o.Window.Close(); }; /// Triggers.Of.AllWindows(); /// /// //window triggers. Note: window triggers don't depend on Triggers.Of. /// /// win[TWEvent.ActiveNew, "* Notepad", "Notepad"] = o => print.it("opened Notepad window"); /// win[TWEvent.ActiveNew, "Notepad", "#32770", contains: "Do you want to save *"] = o => { /// print.it("opened Notepad's 'Do you want to save' dialog"); /// //keys.send("Alt+S"); //click the Save button /// }; /// /// //autotext triggers /// /// tt["los"] = o => o.Replace("Los Angeles"); /// tt["WIndows", TAFlags.MatchCase] = o => o.Replace("Windows"); /// tt.DefaultPostfixType = TAPostfix.None; /// tt[""] = o => o.Replace("[[|]]"); /// tt["#file"] = o => { /// o.Replace(""); /// var fd = new FileOpenSaveDialog(); /// if(fd.ShowOpen(out string file)) keys.sendt(file); /// }; /// tt.DefaultPostfixType = default; /// /// //shorter auto-replace code /// /// var ts = Triggers.Autotext.SimpleReplace; /// ts["#so"] = "Some text"; //the same as tt["#so"] = o => o.Replace("Some text"); /// ts["#mo"] = "More text"; /// /// //how to set opt options for trigger actions /// /// Triggers.Options.BeforeAction = o => { opt.key.TextHow = OKeyText.Paste; }; //sets opt before executing an action /// ts["#p1"] = "text 1"; /// ts["#p2"] = "text 2"; /// Triggers.Options.BeforeAction = null; /// /// //initial opt for ALL trigger actions also can be set ONCE when adding triggers. Use Triggers.Options.BeforeAction if want to override some options for some trigger actions. /// /// opt.key.TextHow = OKeyText.Paste; /// //then add all triggers /// /// //how to stop and disable/enable triggers /// /// hk["Ctrl+Alt+Q"] = o => Triggers.Stop(); //let Triggers.Run() end its work and return /// hk.Last.EnabledAlways = true; /// /// hk["Ctrl+Alt+D"] = o => Triggers.Disabled ^= true; //disable/enable triggers here /// hk.Last.EnabledAlways = true; /// /// hk["Ctrl+Alt+Win+D"] = o => ActionTriggers.DisabledEverywhere ^= true; //disable/enable triggers in all processes /// hk.Last.EnabledAlways = true; /// /// hk["Ctrl+F7"] = o => print.it("This trigger can be disabled/enabled with Ctrl+F8."); /// var t1 = hk.Last; /// hk["Ctrl+F8"] = o => t1.Disabled ^= true; //disable/enable a trigger /// /// //finally call Triggers.Run(). Without it the triggers won't work. /// Triggers.Run(); /// //Triggers.Run returns when is called Triggers.Stop (see the "Ctrl+Alt+Q" trigger above). /// print.it("called Triggers.Stop"); /// ]]> /// public partial class ActionTriggers { readonly ITriggers[] _t; ITriggers this[TriggerType e] => _t[(int)e]; /// /// Initializes a new instance of this class. /// public ActionTriggers() { _t = new ITriggers[4]; scopes_ = new TriggerScopes(); funcs_ = new TriggerFuncs(); options_ = new TriggerOptions(); } //public TriggerScopes Of { // get { // if(_test == false){ // Debug_.MemorySetAnchor_(); // timer.after(500, _ => Debug_.MemoryPrint_()); // } // var k=new StackFrame(1, true); // //new StackTrace(1, true); // var s = k.GetFileName(); // //if(_test == false) _s1 = s; else print.it(ReferenceEquals(s, _s1)); // _test = true; // return scopes; // } //} //static bool _test; //static string _s1; /// /// Allows to set window scopes (working windows) for triggers. /// /// Examples: , . public TriggerScopes Of => scopes_; internal readonly TriggerScopes scopes_; /// /// Allows to set custom scopes/contexts/conditions for triggers. /// /// More info and examples: , . public TriggerFuncs FuncOf => funcs_; internal readonly TriggerFuncs funcs_; /// /// Allows to set some options for multiple triggers and their actions. /// /// More info and examples: , . public TriggerOptions Options => options_; internal readonly TriggerOptions options_; /// /// Clears all options (of , , , ). /// public void ResetOptions() { Of.AllWindows(); FuncOf.Reset(); Options.Reset(); Autotext.ResetOptions(); } ITriggers _Get(TriggerType e) { int i = (int)e; var t = _t[i]; if (t == null) { switch (e) { case TriggerType.Hotkey: t = new HotkeyTriggers(this); break; case TriggerType.Autotext: t = new AutotextTriggers(this); break; case TriggerType.Mouse: t = new MouseTriggers(this); break; case TriggerType.Window: t = new WindowTriggers(this); break; default: Debug.Assert(false); break; } _t[i] = t; } return t; } /// /// Hotkey triggers. /// /// See . public HotkeyTriggers Hotkey => _Get(TriggerType.Hotkey) as HotkeyTriggers; /// /// Autotext triggers. /// /// See . public AutotextTriggers Autotext => _Get(TriggerType.Autotext) as AutotextTriggers; /// /// Mouse triggers. /// /// See . public MouseTriggers Mouse => _Get(TriggerType.Mouse) as MouseTriggers; /// /// Window triggers. /// /// See . public WindowTriggers Window => _Get(TriggerType.Window) as WindowTriggers; /// /// Makes triggers alive. /// /// /// This function monitors hotkeys, activated windows and other events. When an event matches an added trigger, launches the trigger's action, which runs in other thread by default. /// Does not return immediately. Runs until this process is terminated or called. /// /// See . /// Already running. /// Something failed. public unsafe void Run() { //Debug_.PrintLoadedAssemblies(true, true, true); ThrowIfRunning_(); //bool haveTriggers = false; HooksThread.UsedEvents hookEvents = 0; _windowTriggers = null; for (int i = 0; i < _t.Length; i++) { var t = _t[i]; if (t == null || !t.HasTriggers) continue; //haveTriggers = true; switch ((TriggerType)i) { case TriggerType.Hotkey: hookEvents |= HooksThread.UsedEvents.Keyboard; break; case TriggerType.Autotext: hookEvents |= HooksThread.UsedEvents.Keyboard | HooksThread.UsedEvents.Mouse; break; case TriggerType.Mouse: hookEvents |= (t as MouseTriggers).UsedHookEvents_; break; case TriggerType.Window: _windowTriggers = t as WindowTriggers; break; } } //print.it(haveTriggers, (uint)llHooks); //if(!haveTriggers) return; //no. The message loop may be used for toolbars etc. if (!s_wasRun) { s_wasRun = true; WndUtil.RegisterWindowClass(c_cn); } _wMsg = WndUtil.CreateMessageOnlyWindow(_WndProc, c_cn); _mainThreadId = Api.GetCurrentThreadId(); _winTimerPeriod = 0; _winTimerLastTime = 0; if (hookEvents != 0) { //prevent big delay (JIT) later on first LL hook event while hook proc waits if (!s_wasKM) { s_wasKM = true; ThreadPool.QueueUserWorkItem(_ => { try { //using var p1 = perf.local(); new wndFinder("*a").IsMatch(wnd.getwnd.root); //if used window scopes etc _ = WindowsHook.LowLevelHooksTimeout; //slow JIT of registry functions Jit_.Compile(typeof(ActionTriggers), nameof(_WndProc), nameof(_KeyMouseEvent)); Jit_.Compile(typeof(TriggerHookContext), nameof(TriggerHookContext.InitContext), nameof(TriggerHookContext.PerfEnd), nameof(TriggerHookContext.PerfWarn)); Jit_.Compile(typeof(ActionTrigger), nameof(ActionTrigger.MatchScopeWindowAndFunc_)); Jit_.Compile(typeof(HotkeyTriggers), nameof(HotkeyTriggers.HookProc)); AutotextTriggers.JitCompile(); MouseTriggers.JitCompile(); } catch (Exception ex) { Debug_.Print(ex); } }); } _thc = new TriggerHookContext(this); _ht = new HooksThread(hookEvents, _wMsg); } try { _evStop = Api.CreateEvent(false); _StartStopAll(true); IntPtr h = _evStop; _Wait(&h, 1); } finally { if (hookEvents != 0) { _ht.Dispose(); _ht = null; } Api.DestroyWindow(_wMsg); _wMsg = default; Stopping?.Invoke(this, EventArgs.Empty); _evStop.Dispose(); _StartStopAll(false); _mainThreadId = 0; _threads?.Dispose(); _threads = null; } void _StartStopAll(bool start) { foreach (var t in _t) { if (t?.HasTriggers == true) t.StartStop(start); } } } /// /// Executes in new thread and waits like . /// public void RunThread() { using var th = run.thread(out _, out _, Run); wait.forHandle(0, WHFlags.DoEvents, th.DangerousGetHandle()); } int _mainThreadId; wnd _wMsg; HooksThread _ht; TriggerHookContext _thc; static bool s_wasRun, s_wasKM; const string c_cn = "Au.Triggers.Hooks"; nint _WndProc(wnd w, int message, nint wParam, nint lParam) { try { switch (message) { case Api.WM_USER + 1: //_ht.Return((int)wParam, false); //test speed without _KeyMouseEvent _KeyMouseEvent((int)wParam, (HooksThread.UsedEvents)lParam); return 0; //case Api.WM_USER + 2: //rejected // ShowTriggersListWindow((int)wParam); // return 0; case Api.WM_USER + 10: //run any action if(((GCHandle)lParam).Target is Action ac) { ((GCHandle)lParam).Free(); ac(); } return 0; } } catch (Exception ex) { Debug_.Print(ex); return default; } return Api.DefWindowProc(w, message, wParam, lParam); } unsafe void _KeyMouseEvent(int messageId, HooksThread.UsedEvents eventType) { //perf.first(); //perf.next(); _thc.InitContext(); //perf.next(); bool eat = false; if (eventType == HooksThread.UsedEvents.Keyboard) { //print.it("key"); if (!_ht.GetKeyData(messageId, out var data)) return; var k = new HookData.Keyboard(null, (nint)(&data)); //TODO3: now probably can be simplified, because not using a server _thc.InitMod(k); if (this[TriggerType.Hotkey] is HotkeyTriggers tk) { //if not null eat = tk.HookProc(k, _thc); } if (!eat /*&& _thc.trigger == null*/ && this[TriggerType.Autotext] is AutotextTriggers ta) { ta.HookProc(k, _thc); } } else if (eventType == HooksThread.UsedEvents.MouseEdgeMove) { //print.it("edge/move"); if (this[TriggerType.Mouse] is MouseTriggers tm) { if (!_ht.GetEdgeMoveData(messageId, out var data)) return; tm.HookProcEdgeMove(data, _thc); } } else { //print.it("click/wheel"); if (this[TriggerType.Mouse] is MouseTriggers tm) { if (!_ht.GetClickWheelData(messageId, out var data, out int message)) return; var k = new HookData.Mouse(null, message, (nint)(&data)); eat = tm.HookProcClickWheel(k, _thc); } } //perf.next(); _thc.PerfWarn(); //perf.next(); //var mem = GC.GetTotalMemory(false); //if(mem != _debugMem && _debugMem != 0) print.it(mem - _debugMem); //_debugMem = mem; if (!_ht.Return(messageId, eat)) return; //perf.nw(); if (_thc.trigger != null) RunAction_(_thc.trigger, _thc.args, _thc.muteMod); } //long _debugMem; internal void RunAction_(ActionTrigger trigger, TriggerArgs args, int muteMod = 0) { if (trigger.action != null) { _threads ??= new TriggerActionThreads(); _threads.Run(trigger, args, muteMod); } else Debug.Assert(muteMod == 0); } TriggerActionThreads _threads; /// /// Stops trigger engines and causes to return. /// /// /// Does not abort threads of trigger actions that are still running. /// /// /// Note: the Triggers in examples is a field or property like readonly ActionTriggers Triggers = new();. /// print.it("Ctrl+T"); /// Triggers.Hotkey["Ctrl+Q"] = o => { print.it("Ctrl+Q (stop)"); Triggers.Stop(); }; /// Triggers.Hotkey.Last.EnabledAlways = true; /// Triggers.Run(); /// print.it("stopped"); /// ]]> /// public void Stop() { Api.SetEvent(_evStop); } Handle_ _evStop; /// /// Occurs before stops trigger engines and returns. /// public event EventHandler Stopping; /// /// True if executing . /// internal bool Running_ => !_evStop.Is0; /// /// Throws if executing . /// internal void ThrowIfRunning_() { if (Running_) throw new InvalidOperationException("Must be before or after Run."); } /// /// Throws if not executing . /// internal void ThrowIfNotRunning_() { if (!Running_) throw new InvalidOperationException("Cannot be before or after Run."); } /// /// Throws if not thread of . /// internal void ThrowIfNotMainThread_() { if (!IsMainThread) throw new InvalidOperationException("Must be in thread of Run (for example in a FuncOf function)."); } /// /// Returns true in thread of . /// internal bool IsMainThread => Api.GetCurrentThreadId() == _mainThreadId; /// /// Gets or sets whether triggers of this instance are disabled. /// /// /// Does not depend on . /// Does not end/pause threads of trigger actions. /// /// /// /// /// Note: the Triggers in examples is a field or property like readonly ActionTriggers Triggers = new();. /// print.it("Ctrl+T"); /// Triggers.Hotkey["Ctrl+D"] = o => { print.it("Ctrl+D (disable/enable)"); Triggers.Disabled ^= true; }; //toggle /// Triggers.Hotkey.Last.EnabledAlways = true; /// Triggers.Run(); /// ]]> /// public bool Disabled { get; set; } /// /// Gets or sets whether triggers are disabled in all processes that use this library in this user session. /// /// See . /// /// public static unsafe bool DisabledEverywhere { get => SharedMemory_.Ptr->triggers.disabled; set { if (value == DisabledEverywhere) return; SharedMemory_.Ptr->triggers.disabled = value; var w = ScriptEditor.WndMsg_; if (!w.Is0) w.SendNotify(Api.WM_USER, 20); //update tray icon etc } } [StructLayout(LayoutKind.Sequential, Size = 16)] //note: this struct is in shared memory. Size must be same in all library versions. internal struct SharedMemoryData_ { public bool disabled; public bool resetAutotext; } /// /// Sends a sync or async message to _wMsg (_WndProc in the triggers thread). /// internal void SendMsg_(bool sync, int message, nint wParam = 0, nint lParam = 0) { if (sync) _wMsg.Send(message, wParam, lParam); else _wMsg.SendNotify(message, wParam, lParam); } /// /// Sends a sync or async message to _wMsg (_WndProc in the triggers thread). /// internal void SendMsg_(bool sync, Action a) { SendMsg_(sync, Api.WM_USER + 10, 0, (nint)GCHandle.Alloc(a)); } unsafe int _Wait(IntPtr* ha, int nh) { for (; ; ) { int slice = -1; if (_winTimerPeriod > 0) { long t = perf.ms; if (_winTimerLastTime == 0) _winTimerLastTime = t; int td = (int)(t - _winTimerLastTime); int period = _Period(); if (td >= period - 5) { _winTimerLastTime = t; _windowTriggers?.Timer_(); slice = _Period(); } else slice = period - td; int _Period() => _winTimerPeriod / 15 * 15 + 10; //This code is a variable-frequency timer that uses less CPU than Windows timer. // Never mind: the timer does not work if user code creates a nested message loop in this thread. They should avoid it. It is documented, such functions must return ASAP. } var k = Api.MsgWaitForMultipleObjectsEx(nh, ha, slice, Api.QS_ALLINPUT, Api.MWMO_ALERTABLE | Api.MWMO_INPUTAVAILABLE); if (k == nh) { //message, COM, hook, etc if (!wait.doEvents()) return -1; } else if (k is not (Api.WAIT_TIMEOUT or Api.WAIT_IO_COMPLETION)) return k; //signaled handle, abandoned mutex, WAIT_FAILED (-1) } } long _winTimerLastTime; WindowTriggers _windowTriggers; internal int WinTimerPeriod_ { get => _winTimerPeriod; set { long t = perf.ms; int td = (int)(t - _winTimerLastTime); if (td > 10) _winTimerLastTime = t; _winTimerPeriod = value; } } int _winTimerPeriod; } enum TriggerType { Hotkey, Autotext, Mouse, Window, } interface ITriggers { /// /// Return true if added triggers of this type. /// bool HasTriggers { get; } /// /// Optionally start/stop the trigger engine (hooks etc). /// /// void StartStop(bool start); } class TriggerHookContext : WFCache { //internal readonly ActionTriggers triggers; wnd _w; bool _haveWnd, _mouseWnd; POINT _p; public TriggerHookContext(ActionTriggers triggers) { //this.triggers = triggers; _perfList = new _ScopeTime[32]; base.CacheName = true; //we'll call Clear(onlyName: true) at the start of each event } public wnd Window { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { if (!_haveWnd) { _haveWnd = true; _w = _mouseWnd ? wnd.fromXY(_p, WXYFlags.NeedWindow) : wnd.active; } return _w; } } /// /// Trigger/action to run. Set by a hook proc of a trigger engine. /// public ActionTrigger trigger; /// /// Used with . /// public TriggerArgs args; /// /// Used with . /// Can be 0 or one of TriggerActionThreads.c_ constants. /// public int muteMod; ///// ///// This event was processed (not ignored). Set by a hook proc of a trigger engine. ///// //public bool processed; /// /// Called before processing each hook event. Clears most properties and fields. /// public void InitContext() { _w = default; _haveWnd = _mouseWnd = false; base.Clear(onlyName: true); trigger = null; args = null; muteMod = 0; _perfLen = 0; } /// /// Tells to get window (for scope) from the specified point. If not called, will use the active window. In any case, gets window on demand. /// public void UseWndFromPoint(POINT p) { _mouseWnd = true; _p = p; } struct _ScopeTime { public int time_, avgTime; } long _perfTime; _ScopeTime[] _perfList; //don't use List<> because its JIT is too slow in time-critical code int _perfLen; public void PerfStart() { _perfTime = perf.mcs; } public void PerfEnd(bool isFunc, ref int perfTime) { long tLong = perf.mcs - _perfTime; int t = (int)Math.Min(tLong, 1_000_000_000); //calc average time of this scope. Assume the first time is 0. if (perfTime != 0) perfTime = Math.Max(1, (int)(((long)perfTime * 7 + t) / 8)); //print.it($"time={time}, avg={perfTime}"); if (isFunc) t |= unchecked((int)0x80000000); if (_perfLen == _perfList.Length) Array.Resize(ref _perfList, _perfList.Length * 2); _perfList[_perfLen++] = new _ScopeTime { time_ = t, avgTime = perfTime }; if (perfTime == 0) perfTime = 1; } public void PerfWarn() { if (_perfLen == 0) return; long ttTrue = 0, ttCompare = 0; for (int i = 0; i < _perfLen; i++) { var v = _perfList[i]; int t = v.time_ & 0x7fffffff, ta; ttTrue += t; if (v.avgTime == 0) ta = Math.Max(t - 150_000, 0); //first time. Can be slow JIT and assembly loading. else ta = Math.Min(t, v.avgTime); ttCompare += ta; } ttCompare /= 1000; ttTrue /= 1000; //print.it(ttTrue, ttCompare); if (ttCompare <= 25 && (ttTrue < 200 || ttTrue < WindowsHook.LowLevelHooksTimeout - 100)) return; var b = new StringBuilder(); b.AppendFormat("<>Warning: Too slow trigger scope detection (Triggers.Of or Triggers.FuncOf). Time: {0} ms. Task name: {1}. ", ttTrue, script.name); for (int i = 0; i < _perfLen; i++) { var v = _perfList[i]; int t = v.time_ & 0x7fffffff; b.AppendFormat("\t{0} {1,8}.{2:D3} ms", v.time_ < 0 ? "F" : "W", t / 1000, t % 1000); if (v.avgTime > 0) b.AppendFormat(", average {0}.{1:D3} ms", v.avgTime / 1000, v.avgTime % 1000); b.AppendLine(); } b.Append("* W - Triggers.Of (window); F - Triggers.FuncOf."); ThreadPool.QueueUserWorkItem(print.it, b.ToString()); //4 ms first time. Async because JIT slow. } /// /// Currently pressed modifier keys. Valid only in hotkey and autotext triggers. /// public KMod Mod => _mod; /// /// Currently pressed left-side modifier keys. Valid only in hotkey and autotext triggers. /// public KMod ModL => _modL; /// /// Currently pressed right-side modifier keys. Valid only in hotkey and autotext triggers. /// public KMod ModR => _modR; /// /// Not 0 if this key event is a modifier key. Valid only in hotkey and autotext triggers. /// public KMod ModThis => _modThis; KMod _mod, _modL, _modR, _modThis; long _lastKeyTime; /// /// Called before processing each keyboard hook event. /// Updates , , , . They are used by hotkey and autotext triggers. /// public void InitMod(HookData.Keyboard k) { KMod modL = 0, modR = 0; switch (k.vkCode) { case KKey.LCtrl: modL = KMod.Ctrl; break; case KKey.LShift: modL = KMod.Shift; break; case KKey.LAlt: modL = KMod.Alt; break; case KKey.Win: modL = KMod.Win; break; case KKey.RCtrl: modR = KMod.Ctrl; break; case KKey.RShift: modR = KMod.Shift; break; case KKey.RAlt: modR = KMod.Alt; break; case KKey.RWin: modR = KMod.Win; break; } if ((_modThis = (modL | modR)) != 0) { if (k.IsUp) { _modL &= ~modL; _modR &= ~modR; } else { _modL |= modL; _modR |= modR; } _mod = _modL | _modR; } else if (!k.IsUp) { //We cannot trust _mod, because hooks are unreliable. We may not receive some events because of hook timeout, other hooks, OS quirks, etc. Also triggers may start while a modifier key is pressed. //And we cannot use keys.isPressed, because our triggers release modifiers. Also Key() etc. Then triggers could not be auto-repeated. //We use both. If IsPressed(mod), add mod to _mod. Else remove from _mod after >5 s since the last seen key event. The max auto-repeat delay that you can set in CP is ~1 s. TrigUtil.GetModLR(out modL, out modR); //Debug_.PrintIf(modL != _modL || modR != _modR, $"KEY={k.vkCode} modL={modL} _modL={_modL} modR={modR} _modR={_modR}"); //normally should be only when auto-repeating a trigger _modL |= modL; _modR |= modR; long time = Environment.TickCount64; if (time - _lastKeyTime > 5000) { _modL &= modL; _modR &= modR; } _mod = _modL | _modR; //print.it(_mod, k.vkCode); _lastKeyTime = time; } } } ================================================ FILE: Au/Triggers/TriggersListWindow.cs ================================================ using Au.Triggers; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; using System.Windows.Markup; using System.Windows.Threading; using System.Collections.ObjectModel; using System.Windows.Data; namespace Au.Triggers; public partial class ActionTriggers { /// /// Shows a temporary window with a list of currently active triggers for the active window (hotkey, autotext and mouse edge/move triggers) or the mouse window (mouse click/wheel triggers). /// /// Which trigger type to select initially: 0 hotkey, 1 autotext, 2 mouse, 3 window, -1 previous. /// /// To determine which triggers are active in the target window, the function uses window scopes specified in code (like Triggers.Of.Window("Window name")). However it ignores code like Triggers.FuncOf..., therefore the list includes triggers deactivated by your FuncOf functions. /// /// /// Code in file Hotkey triggers.cs. Calls this function when pressed hotkey Alt+?. /// Triggers.ShowTriggersListWindow(); /// ]]> /// public void ShowTriggersListWindow(int triggerType = -1) { run.thread(() => TriggersListWindow.Show_(this, triggerType)); //triggers.SendMsg_(false, () => TriggersListWindow.Show_(this, triggerType)); //no. The triggers thread uses a special message loop etc. Possible various anomalies. } internal bool triggersListWindowIsActive_; } class TriggersListWindow : Window { static TriggersListWindow s_w; internal static void Show_(ActionTriggers triggers, int triggerType) { //single instance if (s_w != null) { s_w.Dispatcher.InvokeAsync(() => { s_w.Hwnd().ActivateL(true); if (triggerType is >= 0 and <= 3) { s_w._arb[triggerType].IsChecked = true; } }); return; } s_w = new(triggers, triggerType); s_w.ShowDialog(); s_w.Dispatcher.InvokeShutdown(); s_w = null; } ActionTriggers _triggers; wnd _wActive, _wMouse; POINT _pMouse; RadioButton[] _arb; ListView _lv; ListCollectionView _view; TriggersListWindow(ActionTriggers triggers, int triggerType) { _triggers = triggers; if (triggerType is < 0 or > 3) triggerType = Math.Clamp(s_sett.triggerType, 0, 3); _pMouse = mouse.xy; _wMouse = wnd.fromXY(_pMouse, WXYFlags.NeedWindow); _wActive = wnd.active; var a = _GetTriggers(); Title = "Triggers"; var b = new wpfBuilder(this).WinSize(500, 700); b.R.AddToolBar_(out _, out var tb, hideOverflow: true, controlBrush: true).Margin("0"); b.Add(out TextBox tFilter).Margin("L0").Focus(); tFilter.PreviewMouseUp += (_, e) => { if (e.ChangedButton == MouseButton.Middle) tFilter.Clear(); }; b.Row(-1).Add(out _lv); _lv.SelectionMode = SelectionMode.Single; VirtualizingStackPanel.SetVirtualizationMode(_lv, VirtualizationMode.Recycling); ScrollViewer.SetHorizontalScrollBarVisibility(_lv, ScrollBarVisibility.Disabled); _SetItemTemplate(); _lv.ItemsSource = a; _lv.MouseUp += (_, e) => { if (e.ChangedButton is MouseButton.Left or MouseButton.Right) { if (s_sett.runOn2Click && e.ChangedButton is MouseButton.Left) return; if (_lv.ContainerFromElement(e.OriginalSource as DependencyObject) is ListViewItem { Content: _TLItem t } lvi) _Menu(t, lvi, 1); } }; _lv.MouseDoubleClick += (_, e) => { if (s_sett.runOn2Click && e.ChangedButton is MouseButton.Left) { if (_lv.ContainerFromElement(e.OriginalSource as DependencyObject) is ListViewItem { Content: _TLItem t } lvi) _Menu(t, lvi, 2); } }; _lv.GotKeyboardFocus += (_, _) => { Dispatcher.InvokeAsync(() => { if (_lv.Items.Count > 0) { int i = _lv.SelectedIndex; if (i < 0) _lv.SelectedIndex = i = 0; _lv.ScrollIntoView(_lv.Items.GetItemAt(i)); if (_lv.ItemContainerGenerator.ContainerFromIndex(i) is ListViewItem lvi) lvi.Focus(); } }); }; b.End(); string filter = null; _view = (ListCollectionView)CollectionViewSource.GetDefaultView(a); _view.Filter = o => { if (o is _TLItem t) { if (_TriggerTypeToInt(t) != triggerType) return false; if (!filter.NE()) { if (!(t.trigger.Contains(filter, StringComparison.OrdinalIgnoreCase) || (t.action?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false))) return false; } } return true; }; if (s_sett.sort) _view.CustomSort = new _SortComparer(); string[] aTriggersTypeStrings = { "_Hotkey", "_Autotext", "_Mouse", "_Window" }; _arb = new RadioButton[aTriggersTypeStrings.Length]; for (int i = 0; i < _arb.Length; i++) { tb.Items.Add(_arb[i] = new RadioButton { Content = new AccessText { Text = aTriggersTypeStrings[i] }, Width = 60, Margin = new(0, 0, 3, 0), BorderBrush = SystemColors.ActiveBorderBrush }); int tt = i; _arb[i].Checked += (_, e) => { s_sett.triggerType = triggerType = tt; _view.Refresh(); }; _arb[i].Focusable = false; //to avoid focusing on access key } _arb[triggerType].IsChecked = true; tFilter.TextChanged += (_, _) => { filter = tFilter.Text; _view.Refresh(); }; SourceInitialized += (_, _) => { var w = this.Hwnd(); w.MoveToScreenCenter(); //workaround for WPF bug: incorrectly centers in a non-primary screen with different DPI }; Loaded += (_, _) => { var w = this.Hwnd(); w.ActivateL(); }; Activated += (_, _) => { _triggers.triggersListWindowIsActive_ = true; }; Deactivated += (_, _) => { _triggers.triggersListWindowIsActive_ = false; if (s_sett.autoClose && IsVisible) timer.after(100, _ => { if (!wnd.active.IsOfThisThread) Close(); }); }; } protected override void OnPreviewKeyDown(KeyEventArgs e) { switch (e.Key) { case Key.Escape: Close(); e.Handled = true; return; case Key.Down when Keyboard.FocusedElement is TextBox: _lv.Focus(); e.Handled = true; return; case Key.Enter: if (e.OriginalSource is ListViewItem { Content: _TLItem t } lvi) { _Menu(t, lvi, 0); } else { _lv.Focus(); timer.after(1, _ => { if (_lv.SelectedItem is _TLItem t && _lv.ItemContainerGenerator.ContainerFromItem(t) is ListViewItem lvi) _Menu(t, lvi, 0); }); } e.Handled = true; return; } base.OnPreviewKeyDown(e); } void _Menu(_TLItem t, ListViewItem li, int clickCount) { if ((s_sett.runOnEnter && clickCount == 0) || (s_sett.runOn2Click && clickCount == 2)) { _RunAction(t); return; } var m = new popupMenu { CheckDontClose = true }; m["&Run"] = o => _RunAction(t); if (clickCount == 0) m.FocusedItem = m.Last; m["&Edit"] = o => { if (s_sett.autoClose) Close(); ScriptEditor.Open(t.t.SourceFile, t.t.SourceLine); }; m.Separator(); m.Submenu("&Settings", m => { m.AddCheck("Auto-close this window", s_sett.autoClose, o => { s_sett.autoClose = o.IsChecked; }); m.Separator(); m.AddCheck("Run on 2*click", s_sett.runOn2Click, o => { s_sett.runOn2Click = o.IsChecked; }); m.AddCheck("Run on single Enter", s_sett.runOnEnter, o => { s_sett.runOnEnter = o.IsChecked; }); m.Separator(); m.AddCheck("Compact list", s_sett.compact, o => { s_sett.compact = o.IsChecked; _SetItemTemplate(); }); m.AddCheck("Sort", s_sett.sort, o => { s_sett.sort = o.IsChecked; _view.CustomSort = s_sett.sort ? new _SortComparer() : null; }); m.AddCheck("Sort hotkeys by modifier", s_sett.sortByMod, o => { s_sett.sortByMod = o.IsChecked; if (s_sett.sort) _view.CustomSort = new _SortComparer(); }); }); m["About"] = o => _About(); var r = li.RectInScreen(); POINT p = clickCount==0 ? new(r.left, r.bottom) : mouse.xy; m.Show(PMFlags.Underline | PMFlags.AlignRectBottomTop | (clickCount==0 ? 0 : PMFlags.AlignCenterH), xy: p, excludeRect: r, owner: this); } void _RunAction(_TLItem t) { if (s_sett.autoClose) Close(); wnd ww = default; if (t.t is WindowTrigger tw) { ww = tw.Finder.Find(); if (ww.Is0) { dialog.showInfo("Cannot run trigger action", $"Window not found.\n\n{tw.Finder}", flags: DFlags.CenterMouse); return; } if (tw.Event is TWEvent.Active or TWEvent.ActiveNew or TWEvent.ActiveOnce || !ww.HasExStyle(WSE.NOACTIVATE)) ww.ActivateL(true); } else if (t.t is MouseTrigger { Kind: TMKind.Click or TMKind.Wheel } mt) { if (_wMouse == _wActive || wnd.fromXY(_pMouse, WXYFlags.NeedWindow) != _wMouse) _wMouse.ActivateL(true); } else { _wActive.ActivateL(true); } if (!s_sett.autoClose && this.Hwnd().IsActive) this.Hwnd().ShowMinimized(1); timer2.after(200, _ => _RunNow(t.t, ww)); void _RunNow(ActionTrigger t, wnd ww) { wnd w, w2; string s2 = null; if (t is WindowTrigger tw) { w = w2 = ww; if (!w.IsVisible) return; if (tw.Event is TWEvent.Active or TWEvent.ActiveNew or TWEvent.ActiveOnce && !w.IsActive) (w2, s2) = (default, "active"); } else { if (t is MouseTrigger { Kind: TMKind.Click or TMKind.Wheel } mt) { try { mouse.move(_pMouse); 100.ms(); } catch { return; } (w, w2, s2) = (_wMouse, wnd.fromXY(_pMouse, WXYFlags.NeedWindow), "mouse"); } else { (w, w2, s2) = (_wActive, wnd.active, "active"); } } if (w2 != w) { dialog.showInfo("Cannot run trigger action", $"The {s2} window must be:\n\n{w}", flags: DFlags.CenterMouse); return; } TriggerArgs ta = t switch { HotkeyTrigger k => new HotkeyTriggerArgs(k, w, 0, 0), AutotextTrigger k => new AutotextTriggerArgs(k, w, "", false), MouseTrigger k => new MouseTriggerArgs(k, w, 0), WindowTrigger k => new WindowTriggerArgs(k, w, 0), _ => null }; t.RunAction(ta); } } void _SetItemTemplate() { _lv.ItemTemplate = (DataTemplate)XamlReader.Parse($$$""" """); } void _About() { var s = $""" The list contains triggers that work in the active window (hotkey, autotext and mouse edge/move triggers) or the mouse window (mouse click/wheel triggers). Active: {_WndToStr(_wActive)} Mouse: {_WndToStr(_wMouse)} Code like `Triggers.Of...` is applied. Code like `Triggers.FuncOf...` is ignored. """; dialog.showInfo("About Triggers", s, owner: this); static string _WndToStr(wnd w) { var name = w.Name; return name.NE() ? $"cn={w.ClassName}, program={w.ProgramName}" : $"name={name}, program={w.ProgramName}"; } } ObservableCollection<_TLItem> _GetTriggers() { List<_TLItem> a = new(); List<(string path, string text, int[] lines)> aFiles = new(); Dictionary dScopes = new(); WFCache wfCache = new() { CacheName = true, NoTimeout = true, IgnoreVisibility = true }; foreach (var t in _triggers.Hotkey) { if (!_InScope(t, _wActive)) continue; string sTrigger = t.ParamsString, sOptions = null; if (t.Flags != 0) { sTrigger = sTrigger[..sTrigger.Find(" (")]; var f1 = t.Flags & (TKFlags.LeftMod | TKFlags.RightMod | TKFlags.Numpad | TKFlags.NumpadNot); //display only flags that are important here if (f1 != 0) sOptions = $" {f1}"; } _Add(t, sTrigger, sOptions); } string postfixKey = _triggers.Autotext.PostfixKey.ToString(); (string s, TAPostfix pt, string pc) atPrev = default; //optimization: don't build identical options foreach (var t in _triggers.Autotext) { if (!_InScope(t, _wActive)) continue; string sOptions = null; if (t.PostfixType == atPrev.pt && t.PostfixChars == atPrev.pc) { sOptions = atPrev.s; } else { if (t.PostfixType != TAPostfix.None) { using (new StringBuilder_(out var b)) { b.Append(" + "); switch (t.PostfixType) { case TAPostfix.Key: b.Append(postfixKey); break; case TAPostfix.Char: b.Append(t.PostfixChars ?? "char"); break; default: b.Append(postfixKey).Append(" or ").Append(t.PostfixChars ?? "char"); break; } sOptions = b.ToString(); } } atPrev = (sOptions, t.PostfixType, t.PostfixChars); } _Add(t, t.Text, sOptions); } foreach (var t in _triggers.Mouse) { if (!_InScope(t, t.Kind is TMKind.Click or TMKind.Wheel ? _wMouse : _wActive)) continue; string s = t.ParamsString, sOptions = null; if (t.Flags != 0) { s = s.RxReplace(@" \(.+?\)", "", 1); var f1 = t.Flags & (TMFlags.LeftMod | TMFlags.RightMod); //display only flags that are important here if (f1 != 0) sOptions = $" {f1}"; } _Add(t, s, sOptions); } foreach (var t in _triggers.Window) { var s2 = " " + t.Event.ToString(); if (t.Later != 0) s2 = s2 + ", later " + t.Later; _Add(t, t.ParamsString, s2); } return new(a.OrderBy(o => _TriggerTypeToInt(o)).ThenBy(o => o.fileIndex).ThenBy(o => o.t.SourceLine)); bool _InScope(ActionTrigger t, wnd w) { if (t.Scope is { } scope) { if (!dScopes.TryGetValue(t.Scope, out bool match)) { dScopes[scope] = match = scope.Match(w, wfCache); } if (!match) return false; } return true; } void _Add(ActionTrigger t, string sTrigger, string sOptions) { var sa = _GetAction(t, out int fileIndex); //print.it($"<>{sTrigger}<> <\a>{sa}"); if (t.Disabled) sOptions += " DISABLED"; a.Add(new(t, sTrigger, sOptions, sa, fileIndex)); } string _GetAction(ActionTrigger t, out int fileIndex) { string text, file = t.SourceFile; int[] lines; for (fileIndex = aFiles.Count; --fileIndex >= 0;) if (aFiles[fileIndex].path == file) break; if (fileIndex < 0) { fileIndex = aFiles.Count; try { text = filesystem.loadText(file); } catch (Exception) { text = ""; } //find line offsets once, not for each trigger (slow) text = text.ReplaceLineEndings("\n"); lines = new int[text.AsSpan().Count('\n') + 1]; for (int j = 1; j < lines.Length; j++) lines[j] = text.IndexOf('\n', lines[j - 1]) + 1; aFiles.Add((file, text, lines)); } else { text = aFiles[fileIndex].text; lines = aFiles[fileIndex].lines; } int sourceLine = t.SourceLine - 1; if ((uint)sourceLine < lines.Length) { int start = lines[sourceLine], end = sourceLine + 1 < lines.Length ? lines[sourceLine + 1] - 1 : text.Length; if (!text.RxMatch(@"\]\s*=\s*(?|\w+\s*=>\s*(.+)|(.+))", 1, out string s, range: start..end)) return null; if (s.Ends("\"\"\"")) { if (text.RxMatch(@"(?s)\s+(.+?)\R\h*""""""", 1, out string s2, range: end..)) s += s2.RxReplace(@"\R\h*", " "); } else if (t is AutotextTrigger && s.Ends(".Menu(")) { if (text.RxMatch(@"(?s)\s+(.+?)\R\h*\);", 1, out string s2, range: end..)) s += s2.RxReplace(@"\R\h*", " "); } return s.Limit(200); } return null; } } static int _TriggerTypeToInt(_TLItem t) => t.t switch { HotkeyTrigger => 0, AutotextTrigger => 1, MouseTrigger => 2, _ => 3 }; record _TLItem(ActionTrigger t, string trigger, string options, string action, int fileIndex) { public ReadOnlySpan GetSortInfo(out int keyWeight, out int modWeight) { if (t is HotkeyTrigger k) { int mod = (int)k.modMask ^ 15 | (int)k.modMasked; modWeight = (((mod & 2) << 3 | (mod & 4) << 3 | (mod & 1) << 6 | (mod & 8) << 4)) >> 4 | System.Numerics.BitOperations.PopCount((uint)mod) << 4; int i = trigger.LastIndexOf('+') + 1; var s = trigger.AsSpan(i).Trim(); keyWeight = s.Length > 1 ? 2 : s[0].IsAsciiAlphaDigit() ? 1 : 0; return s; } keyWeight = modWeight = 0; return trigger; } } class _SortComparer : System.Collections.IComparer { int System.Collections.IComparer.Compare(object o1, object o2) { if (o1 is _TLItem i1 && o2 is _TLItem i2) { var s1 = i1.GetSortInfo(out int keyWeight1, out int modWeight1); var s2 = i2.GetSortInfo(out int keyWeight2, out int modWeight2); if (i1.t is not HotkeyTrigger) return s1.CompareTo(s2, StringComparison.CurrentCultureIgnoreCase); if (s_sett.sortByMod) { int r = modWeight1 - modWeight2; if (r == 0) r = keyWeight1 - keyWeight2; if (r == 0) r = s1.CompareTo(s2, StringComparison.CurrentCultureIgnoreCase); return r; } else { int r = keyWeight1 - keyWeight2; if (r == 0) r = s1.CompareTo(s2, StringComparison.CurrentCultureIgnoreCase); if (r == 0) r = modWeight1 - modWeight2; return r; } } return 0; } } internal record class _Settings : JSettings { public static readonly string File = folders.ThisAppDataRoaming + @"TriggersListWindow settings.json"; public static _Settings Load() => Load<_Settings>(File); public bool autoClose = true; public bool compact; public bool sort, sortByMod; public bool runOnEnter, runOn2Click; public int triggerType; } static readonly _Settings s_sett = _Settings.Load(); } ================================================ FILE: Au/Triggers/Triggers_actions.cs ================================================ namespace Au.Triggers; class TOptions { public Action before; public Action after; public sbyte thread; //>=0 dedicated or <0 TOThread public TOFlags flags; public int wait; public PostToThisThread_ thisThread; public TOptions Clone() => this.MemberwiseClone() as TOptions; } class TOThread { public const sbyte OfTriggers = -1, New = -2, Pool = -3, This = -4; } [Flags] enum TOFlags : byte { NoWarning = 1, Single = 2, //MtaThread = 4, //BackgroundThread=8, //rejected. Always background. Foreground makes no sense here. If need, can easily set in code. } /// /// Allows to set some options for multiple triggers and their actions. /// /// /// You set options through a thread-static property . /// Changed options are applied to all triggers/actions added afterwards in this thread. /// /// /// { opt.key.KeySpeed = 10; }; /// Triggers.Hotkey["Ctrl+K"] = o => print.it(opt.key.KeySpeed); //10 /// Triggers.Hotkey["Ctrl+Shift+K"] = o => print.it(opt.key.KeySpeed); //10 /// Triggers.Options.BeforeAction = o => { opt.key.KeySpeed = 20; }; /// Triggers.Hotkey["Ctrl+L"] = o => print.it(opt.key.KeySpeed); //20 /// Triggers.Hotkey["Ctrl+Shift+L"] = o => print.it(opt.key.KeySpeed); //20 /// ]]> /// public class TriggerOptions { TOptions _new, _prev; TOptions _New() => _new ??= (_prev?.Clone() ?? new TOptions()); /// /// Run actions always in the same dedicated thread that does not end when actions end. /// /// A number that you want to use to identify the thread. Can be 0-127. Default 0. /// Defines when to start an action if an action (other or same) is currently running in this thread. If 0 (default), don't run. If -1 (Timeout.Infinite), run when that action ends (and possibly other queued actions). If > 0, run when that action ends, if it ends within this time from now; the time is in milliseconds. /// No warning when cannot start an action because an action is running and wait == 0. /// /// /// Multiple actions in same thread cannot run simultaneously. Actions in different threads can run simultaneously. /// There is no "end old running action" feature. If need it, use other script. Example: Triggers.Hotkey["Ctrl+M"] = o => script.runWait("Other Script");. /// There is no "temporarily pause old running action to run new action" feature. As well as for scripts. /// The thread has . /// There are several ThreadX functions. Only the last called function is active. If none called, it is the same as called this function without arguments. /// public void Thread(int thread = 0, int wait = 0, bool noWarning = false) { _New(); if ((uint)thread > 127) throw new ArgumentOutOfRangeException(); _new.thread = (sbyte)thread; _new.wait = wait >= -1 ? wait : throw new ArgumentOutOfRangeException(); _new.flags = noWarning ? TOFlags.NoWarning : 0; } //CONSIDER: make default ifRunningWaitMS = 1000 if it is another action. /// /// Run trigger actions in the same thread as . Dangerous, rarely used. /// /// /// This should not be used without a good reason. Trigger actions must be programmed carefully, to not interfere with triggers. They must be as fast as possible, else will block triggers, hooks and user input. /// /// Before v0.16 this was named and used in the "Triggers and toolbars" script. Problem: blocks hooks etc when need long time to get file icons. Now the script uses instead, and calls Triggers.Run in another thread. Your script possibly still uses the old code. You can replace it with the new version, which can be found in menu File > New > Default > Triggers and toolbars. /// public void ThreadOfTriggers() { _New(); _new.thread = TOThread.OfTriggers; _new.wait = 0; _new.flags = 0; } /// /// Alias of . /// [EditorBrowsable(EditorBrowsableState.Never)] //renamed public void ThreadMain() => ThreadOfTriggers(); /// /// Run trigger actions in this thread (which called this function). /// /// /// This function can be used only if runs in another thread. This thread must have a message loop (wait and dispatch messages). For it can be used . /// /// Trigger actions should be fast, else other trigger actions may be delayed. If a trigger action dispatches messages, other trigger actions can run in the meantime. /// /// Can be used to create and show toolbars (). Used in the default "Triggers and toolbars" script since v0.16. /// /// /// { print.it(Environment.CurrentManagedThreadId); }; /// Triggers.RunThread(); /// ]]> /// public void ThreadThis() { _New(); _new.thread = TOThread.This; _new.wait = 0; _new.flags = 0; _new.thisThread = PostToThisThread_.OfThisThread; } /// /// Run trigger actions in new threads. /// /// Don't run if this action is already running. If false, multiple action instances can run parallelly in multiple threads. /// /// The action can run simultaneously with other actions. The thread is STA. /// public void ThreadNew(bool single = false) { _New(); _new.thread = TOThread.New; _new.wait = 0; TOFlags f = 0; if (single) f |= TOFlags.Single; _new.flags = f; } /// /// Run trigger actions in thread pool threads. /// /// Don't run if this action is already running. If false, multiple action instances can run parallelly in multiple threads. /// /// The action can run simultaneously with other actions. May start later if the pool is busy. /// You should know how to use thread pool correctly. The action runs in the .NET thread pool through . /// public void ThreadPool(bool single = false) { _New(); _new.thread = TOThread.Pool; _new.wait = 0; _new.flags = single ? TOFlags.Single : 0; } /// /// A function to run before the trigger action. /// For example, it can set options. /// /// /// { opt.key.KeySpeed = 20; opt.key.TextSpeed = 5; }; /// ]]> /// public Action BeforeAction { set => _New().before = value; } /// /// A function to run after the trigger action. /// For example, it can log exceptions. /// /// /// { if(o.Exception!=null) print.it(o.Exception.Message); else print.it("completed successfully"); }; /// ]]> /// public Action AfterAction { set => _New().after = value; } internal TOptions Current { get { if (_new != null) { _prev = _new; _new = null; } return _prev ?? (s_empty ??= new TOptions()); } } static TOptions s_empty; /// /// If true, triggers added afterwards don't depend on and . /// This property sets the property of triggers added afterwards. /// public bool EnabledAlways { get; set; } /// /// Clears all options. /// public void Reset() { _new = null; _prev = null; } } /// /// Arguments for and . /// public struct TOBAArgs { internal TOBAArgs(TriggerArgs args) { ActionArgs = args; Exception = null; } /// /// Trigger event info. The same variable as passed to the trigger action. /// To access the info, cast to etc, depending on trigger type. /// public TriggerArgs ActionArgs { get; } /// /// If action ended with an exception, the exception. Else null. /// public Exception Exception { get; internal set; } } class TriggerActionThreads { public void Run(ActionTrigger trigger, TriggerArgs args, int muteMod) { //perf.first(); Action actionWrapper = () => { var o = trigger.options; var oldOpt = o.thread is TOThread.New or TOThread.Pool ? default : opt.scope.all(inherit: true); try { _MuteMod(ref muteMod); string sTrigger = null; long startTime = 0; //perf.next(); if (script.role == SRole.MiniProgram) { sTrigger = trigger.ToString(); Api.QueryPerformanceCounter(out startTime); print.TaskEvent_("AS " + sTrigger, startTime, trigger.SourceFile, trigger.SourceLine); //perf.next(); } var baArgs = new TOBAArgs(args); //struct o.before?.Invoke(baArgs); try { //perf.nw(); trigger.Run_(args); if (sTrigger != null) print.TaskEvent_("AE", startTime, trigger.SourceFile, trigger.SourceLine); } catch (Exception e1) { if (sTrigger != null) print.TaskEvent_("AF", startTime, trigger.SourceFile, trigger.SourceLine); baArgs.Exception = e1; print.it(e1); } o.after?.Invoke(baArgs); } catch (Exception e2) { print.it(e2); } finally { oldOpt.Dispose(); if (o.flags.Has(TOFlags.Single)) _d.TryRemove(trigger, out _); if (o.thread is not (TOThread.OfTriggers or TOThread.This)) toolbar.TriggerActionEndedInToolbarUnfriendlyThread_(); } }; //never mind: we should not create actionWrapper if cannot run. But such cases are rare. Fast and small, about 64 bytes. var opt1 = trigger.options; int threadId = opt1.thread; if (threadId >= 0) { //dedicated thread _Thread h = null; foreach (var v in _a) if (v.id == threadId) { h = v; break; } if (h == null) _a.Add(h = new _Thread(threadId)); if (h.RunAction(actionWrapper, trigger)) return; } else if (threadId == TOThread.OfTriggers) { actionWrapper(); return; //note: can reenter. Probably it is better than to cancel if already running. } else if (threadId == TOThread.This) { if (opt1.thisThread.ManagedThreadId == Environment.CurrentManagedThreadId) print.warning("If called ThreadThis, triggers should run in another thread."); opt1.thisThread.Post(actionWrapper); return; //note: can reenter. } else { bool canRun = true; bool single = opt1.flags.Has(TOFlags.Single); if (single) { _d ??= new(); if (_d.TryGetValue(trigger, out var tt)) { switch (tt) { case Thread thread: if (thread.IsAlive) canRun = false; break; case Task task: //print.it(task.Status); switch (task.Status) { case TaskStatus.RanToCompletion: case TaskStatus.Faulted: case TaskStatus.Canceled: break; default: canRun = false; break; } break; } } } if (canRun) { if (threadId == TOThread.New) { var thread = new Thread(actionWrapper.Invoke) { IsBackground = true }; //if (!opt1.flags.Has(TOFlags.MtaThread)) thread.SetApartmentState(ApartmentState.STA); if (single) _d[trigger] = thread; try { thread.Start(); } catch (OutOfMemoryException) { //too many threads, probably 32-bit process if (single) _d.TryRemove(trigger, out _); _OutOfMemory(); //TODO3: before starting thread, warn if there are too many action threads. // In 32-bit process normally fails at ~3000 threads. // Unlikely to fail in 64-bit process, but at ~15000 threads starts to hang temporarily, which causes hook timeout, slow mouse, other anomalies. } } else { //thread pool var task = new Task(actionWrapper); if (single) _d[trigger] = task; task.Start(); } return; } } if (muteMod != 0) ThreadPool.QueueUserWorkItem(_ => _MuteMod(ref muteMod)); } public void Dispose() { foreach (var v in _a) v.Dispose(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] static void _OutOfMemory() { print.warning("There is not enough memory available to start the trigger action thread.", -1); //info: -1 because would need much memory for stack trace } List<_Thread> _a = new(); ConcurrentDictionary _d; class _Thread { record struct _Action(Action actionWrapper, long time); AutoResetEvent _event; Queue<_Action> _q; bool _running; bool _disposed; public readonly int id; public _Thread(int id) { this.id = id; } /// /// Adds the action to the queue and notifies the thread to execute it. /// If the thread is busy, returns false; if ifRunning!=0, the action possibly will run later. /// public bool RunAction(Action actionWrapper, ActionTrigger trigger) { if (_disposed) return false; if (_q == null) { _q = new Queue<_Action>(); _event = new(false); try { run.thread(() => { try { while (!_disposed && _event.WaitOne()) { while (!_disposed) { _Action x; lock (_q) { g1: if (_q.Count == 0) { _running = false; break; } x = _q.Dequeue(); if (x.time != 0 && perf.ms > x.time) goto g1; _running = true; } x.actionWrapper(); } } } finally { _event.Dispose(); _event = null; _q = null; _running = false; //restart if aborted //print.it("thread ended"); } }); } catch (OutOfMemoryException) { //too many threads, probably 32-bit process _event.Dispose(); _event = null; _OutOfMemory(); } } bool R = true; lock (_q) { int ifRunningWaitMS = trigger.options.wait; if (_running) { if (ifRunningWaitMS == 0) { if (!trigger.options.flags.Has(TOFlags.NoWarning)) print.it($"<>Warning: can't run the trigger action because an action is running in this thread. Trigger<>: {trigger}." + " \tTo run simultaneously or wait, use one of Triggers.Options.ThreadX functions.\r\n\tTo disable this warning: Triggers.Options.Thread(noWarning: true);."); return false; } R = false; } else { _running = true; //if(ifRunningWaitMS > 0 && ifRunningWaitMS < 1000000000) ifRunningWaitMS += 1000; } _q.Enqueue(new _Action(actionWrapper, ifRunningWaitMS <= 0 ? 0 : perf.ms + ifRunningWaitMS)); } _event.Set(); return R; } public void Dispose() { if (_disposed) return; _disposed = true; _event.Set(); } } //This old version uses WaitForSingleObject which blocks COM etc, which may cause problems. // In the above code, WaitOne dispatches COM etc. But still little tested. //class _Thread //{ // struct _Action { public Action actionWrapper; public long time; } // Handle_ _event; // Queue<_Action> _q; // bool _running; // bool _disposed; // public readonly int id; // public _Thread(int id) { this.id = id; } // /// // /// Adds the action to the queue and notifies the thread to execute it. // /// If the thread is busy, returns false; if ifRunning!=0, the action possibly will run later. // /// // public bool RunAction(Action actionWrapper, ActionTrigger trigger) // { // if(_disposed) return false; // if(_q == null) { // _q = new Queue<_Action>(); // _event = Api.CreateEvent(false); // try { // run.thread(() => { // try { // while(!_disposed && 0 == Api.WaitForSingleObject(_event, -1)) { // while(!_disposed) { // _Action x; // lock(_q) { // g1: // if(_q.Count == 0) { _running = false; break; } // x = _q.Dequeue(); // if(x.time != 0 && perf.ms > x.time) goto g1; // _running = true; // } // x.actionWrapper(); // } // } // } // finally { // _event.Dispose(); // _q = null; _running = false; //restart if aborted // //print.it("thread ended"); // } // }); // } // catch(OutOfMemoryException) { //too many threads, probably 32-bit process // _event.Dispose(); // _OutOfMemory(); // } // } // bool R = true; // lock(_q) { // int ifRunningWaitMS = trigger.options.ifRunningWaitMS; // if(_running) { // if(ifRunningWaitMS == 0) { // if(!trigger.options.flags.Has(TOFlags.NoWarning)) // print.it("Warning: can't run the trigger action because an action is running in this thread." + // " To run simultaneously or wait, use one of Triggers.Options.ThreadX functions." + // " To disable this warning: Triggers.Options.Thread(noWarning: true);." + // " Trigger: " + trigger); // return false; // } // R = false; // } else { // _running = true; // //if(ifRunningWaitMS > 0 && ifRunningWaitMS < 1000000000) ifRunningWaitMS += 1000; // } // _q.Enqueue(new _Action { actionWrapper = actionWrapper, time = ifRunningWaitMS <= 0 ? 0 : perf.ms + ifRunningWaitMS }); // } // Api.SetEvent(_event); // return R; // } // public void Dispose() // { // if(_disposed) return; _disposed = true; // Api.SetEvent(_event); // } //} static void _MuteMod(ref int muteMod) { switch (Interlocked.Exchange(ref muteMod, 0)) { case c_modRelease: keys.Internal_.ReleaseModAndDisableModMenu(dontThrow: true); break; case c_modCtrl: keys.Internal_.SendKey(KKey.Ctrl, dontThrow: true); //disable Alt/Win menu break; } } public const int c_modRelease = 1, c_modCtrl = 2; } ================================================ FILE: Au/Triggers/Triggers_hooks.cs ================================================ //Key/mouse/autotext triggers use low-level keyboard and mouse hooks. The hooks are in a separate thread, because: // 1. Safer when user code is slow or incorrect. // 2. Works well with COM. In LL hook procedure some COM functions fail, eg find elm with some windows. // Error "An outgoing call cannot be made since the application is dispatching an input-synchronous call". Like when using SendMessage for IPC. // It is important because scripts often use UI elements in scope context functions etc that run in the main thread. //Low-level key/mouse hooks also have other problems, but not too big: // 1. UAC. Hooks of non-admin processes don't work when an admin window is active. // Workaround: let users don't use triggers in non-admin processes. Editor normally is admin, and by default creates task processes of same UAC IL. // 2. Scripts may use raw input or directX, and Windows has this bug: then low-level keyboard hook does not work in that process. // Workaround: let users run such scripts in separate processes. // 3. Each LL hook uses CPU on key/mouse events. // Workaround: let users put all triggers in single script. But it's OK to use another script temporarily eg for testing. // Actually in real conditions even 10 hooks don't use a significant part of CPU compared to CPU used by the target app and OS. //Rejected: single hook server in editor process. It would mitigate some of these problems. Tested. Much code and little benefit. //For window triggers we use winevent hooks. They use less CPU for IPC. // Tested: renaming a toolwindow with no title bar every 1-2 ms in loop: // CPU usage with no winevent hooks is 4%. With 1 hook - 7%. With 10 hooks in different processes - 7%. namespace Au.Triggers; /// /// Thread containing low-level keyboard and mouse hooks. /// class HooksThread : IDisposable { [Flags] public enum UsedEvents { Keyboard = 1, //Hotkey and Autotext triggers Mouse = 2, //Mouse and Autotext triggers. Just sets the hook and resets autotext; to receive events, also add other MouseX flags. MouseClick = 0x10, //Mouse click triggers MouseWheel = 0x20, //Mouse wheel triggers MouseEdgeMove = 0x40, //Mouse edge and move triggers } int _tid; UsedEvents _usedEvents; wnd _wMsg; MouseTriggers.EdgeMoveDetector_ _emDetector; Handle_ _eventStartStop = Api.CreateEvent(false); public HooksThread(UsedEvents usedEvents, wnd wMsg) { _usedEvents = usedEvents; _wMsg = wMsg; run.thread(_Thread, sta: false); //important: not STA, because we use lock, which dispatches sent messages if STA Api.WaitForSingleObject(_eventStartStop, -1); } public void Dispose() { Api.PostThreadMessage(_tid, Api.WM_QUIT, 0, 0); Api.WaitForSingleObject(_eventStartStop, -1); _eventStartStop.Dispose(); _eventSendData.Dispose(); } void _Thread() { _tid = Api.GetCurrentThreadId(); WindowsHook hookK = null, hookM = null; if (_usedEvents.Has(UsedEvents.Keyboard)) { hookK = WindowsHook.Keyboard(_KeyboardHookProc); //note: if lambda, very slow JIT on first hook event } if (_usedEvents.Has(UsedEvents.Mouse)) { hookM = WindowsHook.MouseRaw_(_MouseHookProc); } if (_usedEvents.Has(UsedEvents.MouseEdgeMove)) { _emDetector = new MouseTriggers.EdgeMoveDetector_(); } //tested: don't need JIT-compiling. nint idTimer = (hookK != null || hookM != null) ? Api.SetTimer(default, 0, 10_000, null) : 0; Api.SetEvent(_eventStartStop); while (Api.GetMessage(out var m)) { if (m.message == Api.WM_TIMER && m.wParam == idTimer) { if (Debugger.IsAttached) continue; hookK?.Restore(); hookM?.Restore(); continue; } Api.DispatchMessage(m); } //print.it("hooks thread ended"); hookK?.Dispose(); hookM?.Dispose(); _emDetector = null; Api.SetEvent(_eventStartStop); } unsafe void _KeyboardHookProc(HookData.Keyboard k) { _keyData = *k.NativeStructPtr_; if (_Send(UsedEvents.Keyboard)) k.BlockEvent(); } unsafe bool _MouseHookProc(nint wParam, nint lParam) { int msg = (int)wParam; if (msg == Api.WM_MOUSEMOVE) { if (_usedEvents.Has(UsedEvents.MouseEdgeMove)) { var mll = (Api.MSLLHOOKSTRUCT*)lParam; if (_emDetector.Detect(mll->pt)) { _emData = _emDetector.result; _Send(UsedEvents.MouseEdgeMove); } } } else { bool wheel = msg is Api.WM_MOUSEWHEEL or Api.WM_MOUSEHWHEEL; if ((_usedEvents.Has(UsedEvents.MouseWheel) && wheel) || (_usedEvents.Has(UsedEvents.MouseClick) && !wheel)) { _mouseMessage = (int)wParam; _mouseData = *(Api.MSLLHOOKSTRUCT*)lParam; return _Send(wheel ? UsedEvents.MouseWheel : UsedEvents.MouseClick); } } return false; } /// /// Sends key/mouse event data (copied to _keyData etc) to the main thread. /// Returns true to eat (block, discard) the event. /// On 1100 ms timeout returns false. /// bool _Send(UsedEvents eventType) { //using var p1 = perf.local(); bool ok = _wMsg.SendNotify(Api.WM_USER + 1, _messageId, (int)eventType); bool timeout = Api.WaitForSingleObject(_eventSendData, 1100) == Api.WAIT_TIMEOUT; lock (this) { if (timeout) timeout = Api.WaitForSingleObject(_eventSendData, 0) == Api.WAIT_TIMEOUT; //other thread may SetEvent between WaitForSingleObject and lock _messageId++; return _eat && !timeout; } //info: HookWin._HookProcLL will print warning if > LowLevelHooksTimeout-50. Max LowLevelHooksTimeout is 1000. } //fields for passing key/mouse event data to the main thread and getting its return value Handle_ _eventSendData = Api.CreateEvent(false); //sync int _messageId; //sync bool _eat; //return value Api.KBDLLHOOKSTRUCT _keyData; Api.MSLLHOOKSTRUCT _mouseData; int _mouseMessage; MouseTriggers.EdgeMoveDetector_.Result _emData; /// /// Called by the main thread to resume the hooks thread (_Send) and pass the return value (eat). /// Returns false on timeout. /// public bool Return(int messageId, bool eat) { lock (this) { if (messageId != _messageId) return false; _eat = eat; Api.SetEvent(_eventSendData); } return true; } /// /// Called by the main thread to get key event data sent by _Send. /// Returns false on timeout. /// public bool GetKeyData(int messageId, out Api.KBDLLHOOKSTRUCT data) { data = _keyData; return messageId == _messageId; } /// /// Called by the main thread to get mouse click/wheel event data sent by _Send. /// Returns false on timeout. /// public bool GetClickWheelData(int messageId, out Api.MSLLHOOKSTRUCT data, out int message) { data = _mouseData; message = _mouseMessage; return messageId == _messageId; } /// /// Called by the main thread to get mouse edge/move event data sent by _Send. /// Returns false on timeout. /// public bool GetEdgeMoveData(int messageId, out MouseTriggers.EdgeMoveDetector_.Result data) { data = _emData; return messageId == _messageId; } } ================================================ FILE: Au/Triggers/Triggers_util.cs ================================================ namespace Au.Triggers; static class TrigUtil { /// /// Gets left and right modifiers. Uses . /// Returns modL | modR. /// public static KMod GetModLR(out KMod modL, out KMod modR) { KMod L = 0, R = 0; if (keys.isPressed(KKey.LCtrl)) L |= KMod.Ctrl; if (keys.isPressed(KKey.LShift)) L |= KMod.Shift; if (keys.isPressed(KKey.LAlt)) L |= KMod.Alt; if (keys.isPressed(KKey.Win)) L |= KMod.Win; if (keys.isPressed(KKey.RCtrl)) R |= KMod.Ctrl; if (keys.isPressed(KKey.RShift)) R |= KMod.Shift; if (keys.isPressed(KKey.RAlt)) R |= KMod.Alt; if (keys.isPressed(KKey.RWin)) R |= KMod.Win; modL = L; modR = R; return L | R; } } ================================================ FILE: Au/Triggers/Types/t-autotext.cs ================================================ //CONSIDER: trigger type: When pressed CapsLock, eat trigger text. When triggered, turn off CapsLock. // To cancel, user can turn off CapsLock. // Display typed text in OSD. Allow Backspace. // Eg I can use it for tags instead of Alt+B etc. // Allow to use instead of CapsLock: ScrollLock, Insert. Or not, because unavailable in many keyboards. // Another way: press CapsLock, type text, press CapsLock again (then triggers). namespace Au.Triggers; /// /// Flags of autotext triggers. /// /// /// To avoid passing flags to each trigger as the flags parameter, use ; its initial value is 0, which means: case-insensitive, erase the typed text with Backspace, modify the replacement text depending on the case of the typed text. /// [Flags] public enum TAFlags : byte { /// /// Case-sensitive. /// MatchCase = 1, /// /// Let don't erase the user-typed text. /// Without this flag it erases text with the Backspace key or selects with Shift+Left. If Replace not called, text is not erased/selected regardless of this flag. /// DontErase = 2, /// /// Let don't modify the replacement text. ///
Without ReplaceRaw or MatchCase it: ///
• If the first character of the typed text is uppercase, makes the first character of the replacement text uppercase. ///
• If all typed text is uppercase, makes the replacement text uppercase. ///
ReplaceRaw = 4, /// /// Let remove the postfix delimiter character. /// RemovePostfix = 8, /// /// Let call and do nothing if it returns false. /// Confirm = 16, /// /// Let select text with Shift+Left instead of erasing with Backspace. Except in console windows. /// See also . /// ShiftLeft = 32, } /// /// Postfix type of autotext triggers. /// The trigger action runs only when the user ends the autotext with a postfix character or key, unless postfix type is None. /// Default: CharOrKey. /// public enum TAPostfix : byte { /// A postfix character (see Char) or key (see Key). CharOrKey, /// A postfix character specified in the postfixChars parameter or property. If not specified - any non-word character. Char, /// The Ctrl or Shift key. Default is Ctrl. You can change it with . Key, /// Don't need a postfix. The action runs immediately when the user types the autotext. None, } /// /// See ; /// /// public record class TAMenuOptions(PMFlags pmFlags = PMFlags.ByCaret); /// /// Represents an autotext trigger. /// public class AutotextTrigger : ActionTrigger { readonly string _paramsString; internal readonly TAMenuOptions menuOptions; /// public string Text { get; } /// public TAFlags Flags { get; } /// public TAPostfix PostfixType { get; } /// public string PostfixChars { get; } internal AutotextTrigger(ActionTriggers triggers, Action action, string text, TAFlags flags, TAPostfix postfixType, string postfixChars, TAMenuOptions menuOptions, (string, int) source) : base(triggers, action, true, source) { Text = text; Flags = flags; PostfixType = postfixType; PostfixChars = postfixChars; this.menuOptions = menuOptions; if (flags == 0 && postfixType == 0 && postfixChars == null) { _paramsString = text; } else { using (new StringBuilder_(out var b)) { b.Append(text); if (flags != 0) b.Append(" (").Append(flags.ToString()).Append(')'); if (postfixType != 0) b.Append(" postfixType=").Append(postfixType.ToString()); if (postfixChars != null) b.Append(" postfixChars=").Append(postfixChars); _paramsString = b.ToString(); } } //print.it(this); } internal override void Run_(TriggerArgs args) => RunT_(args as AutotextTriggerArgs); /// /// Returns "Autotext". /// public override string TypeString => "Autotext"; /// /// Returns a string containing trigger parameters. /// public override string ParamsString => _paramsString; } /// /// Autotext triggers. /// /// See . public class AutotextTriggers : ITriggers, IEnumerable { ActionTriggers _triggers; Dictionary _d = new(); internal AutotextTriggers(ActionTriggers triggers) { _triggers = triggers; _simpleReplace = new TASimpleReplace(this); } /// /// Adds an autotext trigger. /// /// The action runs when the user types this text and a postfix character or key. By default case-insensitive. /// Options. If omitted or null, uses . Some flags are used by . /// Postfix type (character, key, any or none). If omitted or null, uses ; default - a non-word character or the Ctrl key. /// Postfix characters used when postfix type is Char or CharOrKey (default). If omitted or null, uses ; default - non-word characters. /// [](xref:caller_info) /// [](xref:caller_info) /// /// - Text is empty or too long. Can be 1 - 100 characters. /// - Postfix characters contains letters or digits. /// /// Cannot add triggers after was called, until it returns. /// See . public Action this[string text, TAFlags? flags = null, TAPostfix? postfixType = null, string postfixChars = null, [CallerFilePath] string f_ = null, [CallerLineNumber] int l_ = 0] { set { _triggers.ThrowIfRunning_(); int len = text.Lenn(); if (len < 1 || len > 100) throw new ArgumentException("Text length must be 1 - 100."); if (text.Contains('\n')) { text = text.RxReplace(@"\r?\n", "\r"); len = text.Length; } TAFlags fl = flags ?? DefaultFlags; bool matchCase = 0 != (fl & TAFlags.MatchCase); if (!matchCase) text = text.Lower(); var t = new AutotextTrigger(_triggers, value, text, fl, postfixType ?? DefaultPostfixType, _CheckPostfixChars(postfixChars) ?? DefaultPostfixChars, MenuOptions, (f_, l_)); //create dictionary key from 1-4 last characters lowercase int k = 0; for (int i = len - 1, j = 0; i >= 0 && j <= 24; i--, j += 8) { var c = text[i]; if (matchCase) c = char.ToLowerInvariant(c); k |= (byte)c << j; } //print.it((uint)k); t.DictAdd_(_d, k); _lastAdded = t; } } /// /// Allows to add triggers in a more concise way - assign a string, not a function. The string will replace the user-typed text. /// /// /// o.Replace("Sunday"); /// ts["#mo"] = "Monday"; /// ]]> /// public TASimpleReplace SimpleReplace => _simpleReplace; TASimpleReplace _simpleReplace; #region options /// /// Default value for the flags parameter used for triggers added afterwards. /// public TAFlags DefaultFlags { get; set; } /// /// Default value for the postfixType parameter used for triggers added afterwards. /// public TAPostfix DefaultPostfixType { get; set; } /// /// Default value for the postfixChars parameter used for triggers added afterwards. /// Default: null. /// /// /// If null (default), postfix characters are all except alpha-numeric (see ). /// The value cannot contain alpha-numeric characters (exception) and characters (triggers will not work). /// For Enter use '\r'. /// /// The value contains letters or digits. public string DefaultPostfixChars { get => _defaultPostfixChars; set => _defaultPostfixChars = _CheckPostfixChars(value); } string _defaultPostfixChars; static string _CheckPostfixChars(string s) { if (s.NE()) return null; int k = 0; for (int i = 0; i < s.Length; i++) { char c = s[i]; if (char.IsLetterOrDigit(c)) throw new ArgumentException("Postfix characters contains letters or digits."); if (c == '\r') k |= 1; if (c == '\n') k |= 2; } if (k == 2) print.warning("Postfix characters contains \\n (Ctrl+Enter) but no \\r (Enter)."); return s; } /// /// The postfix key for all triggers where postfix type is or (default). /// Can be Ctrl (default), Shift, LCtrl, RCtrl, LShift or RShift. /// /// The value is not Ctrl or Shift. /// /// This property is applied to all triggers, not just to those added afterwards. /// public KKey PostfixKey { get => _postfixKey; set { var mod = keys.Internal_.KeyToMod(value); switch (mod) { case KMod.Ctrl: case KMod.Shift: break; default: throw new ArgumentException("Must be Ctrl, Shift, LCtrl, RCtrl, LShift or RShift."); } _postfixMod = mod; _postfixKey = value; } } KKey _postfixKey = KKey.Ctrl; KMod _postfixMod = KMod.Ctrl; /// /// Additional word characters (non-delimiters). /// Default: null. /// /// /// By default, only alpha-numeric characters ( returns true) are considered word characters. You can use this property to add more word characters, for example "_#". /// This is used to avoid activating triggers when a trigger text found inside a word. /// This property is applied to all triggers, not just to those added afterwards. /// public string WordCharsPlus { get; set; } /// /// Options for menus shown by and . /// Used for triggers added afterwards. /// /// /// Show menus by the text cursor. If impossible - in the center of the active window. /// /// /// public TAMenuOptions MenuOptions { get; set; } /// /// Clears all options that affect autotext triggers created after setting the option: , , , . /// public void ResetOptions() { this.DefaultFlags = 0; this.DefaultPostfixType = 0; this._defaultPostfixChars = null; this.MenuOptions = null; //cannot reset these because they are for all triggers, not only for triggers added afterwards //this.PostfixKey = KKey.Ctrl; //this.WordCharsPlus = null; } #endregion /// /// The last added trigger. /// public AutotextTrigger Last => _lastAdded; AutotextTrigger _lastAdded; bool ITriggers.HasTriggers => _lastAdded != null; void ITriggers.StartStop(bool start) { this._len = 0; this._singlePK = false; this._wFocus = default; this._deadKey = default; } internal unsafe void HookProc(HookData.Keyboard k, TriggerHookContext thc) { Debug.Assert(!k.IsInjectedByAu); //server must ignore //print.it(k); //perf.first(); if (ResetEverywhere) { //set by mouse hooks on click left|right and by keyboard hooks on Au-injected key events. In shared memory. ResetEverywhere = false; _Reset(); } if (k.IsUp) { if (_singlePK) { _singlePK = false; if (_IsPostfixMod(thc.ModThis)) { //print.it("< Ctrl up >"); _Trigger(default, true, _GetFocusedWindow(), thc); //goto gReset; //no, resets if triggered, else don't reset } } return; } bool _IsPostfixMod(KMod mod) => mod == _postfixMod && (_postfixKey <= KKey.Ctrl || k.vkCode == _postfixKey) && !k.IsInjected; var modd = thc.ModThis; if (modd != 0) { _singlePK = _IsPostfixMod(modd) && thc.Mod == _postfixMod; return; } _singlePK = false; //TODO3: use KeyToTextConverter. if (k.IsAlt && 0 == (thc.Mod & (KMod.Ctrl | KMod.Shift))) goto gReset; //Alt+key without other modifiers. Info: AltGr can add Ctrl, therefore we process it. Info: still not menu mode. Tested: never types a character, except Alt+numpad numbers. var vk = k.vkCode; if (vk >= KKey.PageUp && vk <= KKey.Down) goto gReset; //PageUp, PageDown, End, Home, Left, Up, Right, Down wnd wFocus = _GetFocusedWindow(); if (wFocus.Is0) goto gReset; var c = stackalloc char[8]; int n; if (vk == KKey.Packet) { c[0] = (char)k.scanCode; n = 1; } else { n = _KeyToChar(c, vk, k.scanCode, wFocus, thc.Mod); if (n == 0) { //non-char key if (thc.Mod == 0) switch (vk) { case KKey.CapsLock: case KKey.NumLock: case KKey.ScrollLock: case KKey.Insert: case KKey.Delete: return; } goto gReset; } if (n < 0) return; //dead key } //print.it(n, c[0], c[1]); for (int i = 0; i < n; i++) _Trigger(c[i], false, wFocus, thc); return; gReset: _Reset(); } static wnd _GetFocusedWindow() { if (!miscInfo.getGUIThreadInfo(out var gt)) return wnd.active; if (0 != (gt.flags & (GTIFlags.INMENUMODE | GTIFlags.INMOVESIZE))) return default; //the character will not be typed when showing menu (or just Alt or F10 pressed) or moving/resizing window. Of course this will not work with nonstandard menus, eg in Word, as well as with other controls that don't accept text. return gt.hwndFocus; //if no focus, the thread will not receive wm-keydown etc } int _len; //count of valid user-typed characters in _text bool _singlePK; //used to detect postfix key (Ctrl or Shift) wnd _wFocus; //the focused window/control. Used to reset if focus changed. void _Reset() { _len = 0; _singlePK = false; //_wFocus = default; } internal static unsafe bool ResetEverywhere { get => SharedMemory_.Ptr->triggers.resetAutotext; set => SharedMemory_.Ptr->triggers.resetAutotext = value; } unsafe void _Trigger(char c, bool isPK, wnd wFocus, TriggerHookContext thc) { //perf.next(); if (wFocus != _wFocus) { _Reset(); _wFocus = wFocus; } if (wFocus.Is0) return; int nc = _len; _DetectedPostfix postfixType; char postfixChar = default; if (isPK) { postfixType = _DetectedPostfix.Key; } else { //print.it((int)c); if (c < ' ' || c == 127) { switch (c) { case (char)8: //Backspace if (_len > 0) _len--; return; case '\t': case '\r': case '\n': break; default: //Ctrl+C etc generate control characters. Also Esc. _Reset(); return; //tested: control codes <32 in most windows don't type characters //tested: Ctrl+Backspace (127) in some windows types a rectangle, in others erases previous word } } bool isWordChar = _IsWordChar(c); postfixType = isWordChar ? _DetectedPostfix.None : _DetectedPostfix.Delim; const int c_bufLen = 127; if (nc >= c_bufLen) { //buffer full. Remove word from beginning. int i; for (i = 0; i < c_bufLen; i++) if (!_text[i].isWordChar) break; if (i == c_bufLen) { if (!isWordChar) { _len = 0; return; } i = c_bufLen - 20; //remove several first chars. Triggers will not match anyway, because max string lenhth is 100. } nc = c_bufLen - ++i; fixed (_Char* p = _text) Api.memmove(p, p + i, nc * sizeof(_Char)); } _text[nc] = new _Char(c, isWordChar); _len = nc + 1; if (isWordChar) nc++; else postfixChar = c; //DebugPrintText(); } if (nc == 0) return; //perf.next(); g1: for (int k = 0, ii = nc - 1, jj = 0; ii >= 0 && jj <= 24; ii--, jj += 8) { //create dictionary key from 1-4 last characters lowercase k |= (byte)_text[ii].cLow << jj; //print.it((uint)k); if (_d.TryGetValue(k, out var v)) { AutotextTriggerArgs args = null; for (; v != null; v = v.next) { var x = v as AutotextTrigger; var s = x.Text; int i = nc - s.Length; if (i < 0) continue; if (i > 0 && _text[i - 1].isWordChar) continue; if (0 != (x.Flags & TAFlags.MatchCase)) { for (int j = 0; i < nc; i++, j++) if (_text[i].c != s[j]) break; } else { for (int j = 0; i < nc; i++, j++) if (_text[i].cLow != s[j]) break; } if (i < nc) continue; switch (x.PostfixType) { case TAPostfix.CharOrKey: if (postfixType == _DetectedPostfix.None) continue; break; case TAPostfix.Char: if (postfixType != _DetectedPostfix.Delim) continue; break; case TAPostfix.Key: if (postfixType != _DetectedPostfix.Key) continue; break; } if (x.PostfixChars != null && postfixType == _DetectedPostfix.Delim && x.PostfixChars.IndexOf(c) < 0) continue; if (v.DisabledThisOrAll) continue; if (_triggers.triggersListWindowIsActive_) continue; if (args == null) { //may need for scope callbacks too bool hasPChar = postfixType == _DetectedPostfix.Delim; int n = s.Length, to = nc; if (hasPChar) { n++; to++; } var tt = new string('\0', n); i = to - n; fixed (char* p = tt) for (int j = 0; i < to;) p[j++] = _text[i++].c; thc.args = args = new AutotextTriggerArgs(x, thc.Window, tt, hasPChar); } else args.Trigger = x; if (!x.MatchScopeWindowAndFunc_(thc)) continue; _Reset(); //CONSIDER: flag DontReset. If the action generates keyboard events or mouse clicks, our kooks will reset. thc.trigger = x; return; } } } //maybe there are items where text ends with delim and no postfix if (postfixType == _DetectedPostfix.Delim) { postfixType = _DetectedPostfix.None; postfixChar = '\0'; nc++; goto g1; } //perf.nw(); //about 90% of time takes _KeyToChar (ToUnicodeEx and GetKeyboardLayout). } unsafe int _KeyToChar(char* c, KKey vk, uint sc, wnd wFocus, KMod mod) { var hkl = Api.GetKeyboardLayout(wFocus.ThreadId); var ks = stackalloc byte[256]; _SetKS(mod); bool win10 = osVersion.minWin10_1607; //the API resets dead key etc, but on new OS flag 4 prevents it int n = Api.ToUnicodeEx((uint)vk, sc, ks, c, 8, win10 ? 4u : 0u, hkl); if (!win10) { //if need, set dead key again var d = stackalloc char[8]; if (_deadKey.vk != 0 && _deadKey.hkl == hkl) { _SetKS(_deadKey.mod); Api.ToUnicodeEx((uint)_deadKey.vk, _deadKey.sc, ks, d, 8, 0, hkl); _deadKey.vk = 0; } else if (n < 0) { _deadKey = new(vk, mod, sc, hkl); Api.ToUnicodeEx((uint)vk, sc, ks, d, 8, 0, hkl); } } void _SetKS(KMod m) { ks[(int)KKey.Shift] = (byte)((0 != (m & KMod.Shift)) ? 0x80 : 0); ks[(int)KKey.Ctrl] = (byte)((0 != (m & KMod.Ctrl)) ? 0x80 : 0); ks[(int)KKey.Alt] = (byte)((0 != (m & KMod.Alt)) ? 0x80 : 0); ks[(int)KKey.Win] = (byte)((0 != (m & KMod.Win)) ? 0x80 : 0); ks[(int)KKey.CapsLock] = (byte)(keys.isCapsLock ? 1 : 0); //don't need this for num lock } return n; //info: this works, but: //1. Does not work with eg Chinese input method. //2. Catches everything that would later be changed by the app, or by a next hook, etc. //3. Don't know how to get Alt+numpad characters. Ignore them. // On Alt up could call tounicodeex with sc with flag 0x8000. It gets the char, but resets keyboard state, and the char is not typed. //4. In console windows does not work with Unicode characters. //if(MapVirtualKeyEx(vk, MAPVK_VK_TO_CHAR, hkl)&0x80000000) { print.it("DEAD"); return -1; } //this cannot be used because resets dead key } _DeadKey _deadKey; record struct _DeadKey(KKey vk, KMod mod, uint sc, nint hkl); //User-typed characters. _len characters are valid. _Char[] _text = new _Char[128]; struct _Char { public char c, cLow; public bool isWordChar; public _Char(char ch, bool isWordChar) { c = ch; cLow = char.ToLowerInvariant(ch); this.isWordChar = isWordChar; } } enum _DetectedPostfix { None, Delim, Key } [MethodImpl(MethodImplOptions.AggressiveInlining)] bool _IsWordChar(char c) { if (char.IsLetterOrDigit(c)) return true; //speed: 4 times faster than Api.IsCharAlphaNumeric. Tested with a string containing 90% ASCII chars. var v = WordCharsPlus; return v != null && v.Contains(c); } [Conditional("DEBUG")] unsafe void _DebugPrintText() { var s = new string(' ', _len); fixed (char* p = s) for (int i = 0; i < s.Length; i++) p[i] = _text[i].c; print.it(s); } internal static unsafe void JitCompile() { Jit_.Compile(typeof(AutotextTriggers), nameof(HookProc), nameof(_Trigger), nameof(_KeyToChar)); } /// /// Used by foreach to enumerate added triggers. /// public IEnumerator GetEnumerator() { foreach (var kv in _d) { for (var v = kv.Value; v != null; v = v.next) { var x = v as AutotextTrigger; yield return x; } } } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } /// /// Arguments for actions of autotext triggers. /// You can use functions and to replace user-typed text. /// public class AutotextTriggerArgs : TriggerArgs { /// public AutotextTrigger Trigger { get; internal set; } /// [EditorBrowsable(EditorBrowsableState.Never)] public override ActionTrigger TriggerBase => Trigger; /// /// The active window. /// public wnd Window { get; } /// /// The user-typed text. If ==true, the last character is the postfix delimiter character. /// public string Text { get; } /// /// true if the autotext activated when the user typed a postfix delimiter character. Then it is the last character in . /// public bool HasPostfixChar { get; } /// /// If true, will select text with Shift+Left instead of erasing with Backspace. Except in console windows. /// /// /// Initially true if flag is set. Can be changed by a callback function, for example to use or not use Shift+Left only with some windows. /// public bool ShiftLeft { get; set; } /// public AutotextTriggerArgs(AutotextTrigger trigger, wnd w, string text, bool hasPChar) { Trigger = trigger; Window = w; Text = text; HasPostfixChar = hasPChar; ShiftLeft = trigger.Flags.Has(TAFlags.ShiftLeft); //print.it($"'{text}'", hasPChar); } /// public override string ToString() => "Trigger: " + Trigger; /// /// Replaces the user-typed text with the specified text or/and HTML. /// /// /// The replacement text. Can be null. /// Can contain [[|]] to move the text cursor (caret) there with the Left key; not if html specified. /// /// /// The replacement HTML. Can be full HTML or fragment. See . /// Can be specified only text or only html or both. If both, will paste html in apps that support it, elsewhere text. If only html, in apps that don't support HTML will paste html as text. /// /// /// Options for this function can be specified when adding triggers, in the flags parameter. Or before adding triggers, with . /// /// /// o.Replace("[[|]]"); /// ]]> /// More examples: . /// public void Replace(string text, string html = null) { if (text == "") text = null; if (html == "") html = null; _Replace(text, html, null); } /// /// Replaces the user-typed text with the specified text, keys, clipboard data, etc. /// /// /// Options for this function can be specified when adding triggers, in the flags parameter. Or before adding triggers, with . This function uses , , , . /// /// If used flag , for label can be used first argument with prefix "!!"; else displays all string arguments. /// /// public void Replace2([ParamString(PSFormat.Keys)] params KKeysEtc[] keysEtc) { Not_.Null(keysEtc); _Replace(null, null, keysEtc); } void _Replace(string r, string html, KKeysEtc[] ke) { bool onlyText = r != null && html == null; var flags = this.Trigger.Flags; string t = this.Text; int caret = -1; if (onlyText) { caret = r.Find("[[|]]"); if (caret >= 0) r = r.Remove(caret, 5); if (!flags.HasAny(TAFlags.ReplaceRaw | TAFlags.MatchCase)) { int len = t.Length; if (this.HasPostfixChar) len--; int i; for (i = 0; i < len; i++) if (char.IsLetterOrDigit(t[i])) break; //eg if t is "#abc", we need a, not # if (i < len && char.IsUpper(t[i])) { bool allUpper = false; //make r ucase if t contains 0 lcase chars and >=2 ucase chars while (++i < len) { var uc = char.GetUnicodeCategory(t[i]); if (uc == UnicodeCategory.LowercaseLetter) { allUpper = false; break; } if (uc == UnicodeCategory.UppercaseLetter) allUpper = true; } r = r.Upper(allUpper ? SUpper.AllChars : SUpper.FirstChar); } } } if (flags.Has(TAFlags.Confirm)) { string confirmText; if (!ke.NE_()) { confirmText = null; if (ke[0].Value is string s2 && s2.Starts("!!")) { confirmText = s2[2..]; ke = ke.RemoveAt(0); } else { foreach (var v in ke) if (v.Value is string s1) { if (confirmText != null) confirmText += ", "; confirmText += s1; } } } else confirmText = r ?? html; if (!Confirm(confirmText)) return; } var k = new keys(opt.key); var optk = k.Options; //UWP is very slow. If text is long and fast: 1. Often does not display part of it until next key. 2. Starts to display with a long delay. // WinUI3 even slower, and has problem 2 but not 1. Because of 2 it's better to send text slower. wnd ww = this.Window.Window; bool uwp = 0 != ww.IsUwpApp || ww.IsWinUI_; //bool uwp = keys.isScrollLock; if (uwp) { optk.TextSpeed = Math.Max(optk.TextSpeed, 10); //default 0 optk.KeySpeed = Math.Max(optk.KeySpeed, 20); //default 2 optk.KeySpeedClipboard = Math.Max(optk.KeySpeedClipboard, 30); //default 5 int n1 = optk.PasteLength - 100; if (n1 > 0) optk.PasteLength = 100 + n1 / 5; //default 200 -> 120 } else { optk.KeySpeed = Math.Clamp(optk.KeySpeed, 2, 20); optk.TextSpeed = Math.Min(optk.TextSpeed, 10); } optk.PasteWorkaround = true; //info: later Options.Hook can override these values. int erase = flags.Has(TAFlags.DontErase) ? (this.HasPostfixChar ? 1 : 0) : t.Length; if (erase > 0) { bool shiftLeft = this.ShiftLeft && !wnd.active.IsConsole; if (shiftLeft) { k.AddKey(KKey.Shift, true); k.AddKey(KKey.Left); } else k.AddKey(KKey.Back); if (erase > 1) k.AddRepeat(erase); if (shiftLeft) k.AddKey(KKey.Shift, false); //note: Back down down ... up does not work with some apps //some apps have async input and eg don't erase all if too fast. // UWP is the champion. Also noticed in Chrome address bar (rare), Dreamweaver when pasting (1/5 times), etc. int sleep = 5 + erase; if (uwp) sleep *= 10; k.AddSleep(sleep); k.Pasting += (_, _) => wait.ms(sleep * 2); } else if (uwp) { k.Pasting += (_, _) => wait.ms(50); } KKey pKey = default; char pChar = default; if (this.HasPostfixChar && !flags.Has(TAFlags.RemovePostfix)) { char ch = t[^1]; if (ch == ' ' || ch == '\r' || ch == '\t') pKey = (KKey)ch; //avoid trimming of pasted text or pasting '\r'; here VK_ == ch. else if (onlyText) r += ch.ToString(); else pChar = ch; } if (ke != null) k.Add(ke); else k.AddText(r, html); if (pKey != default) k.AddKey(pKey); else if (pChar != default) k.AddText(pChar.ToString(), OKeyText.KeysOrChar); if (caret >= 0) { int keyLeft = 0; for (int i = caret; i < r.Length; keyLeft++) { char c = r[i++]; if (c == '\r') { if (i < r.Length && r[i] == '\n') i++; } else if (char.IsHighSurrogate(c)) { if (i < r.Length && char.IsLowSurrogate(r[i])) i++; } } if (pKey != default || pChar != default) keyLeft++; if (keyLeft > 0) { k.AddKey(KKey.Left); if (keyLeft > 1) k.AddRepeat(keyLeft); } } try { k.SendNow(); } catch { } //unlikely } /// /// If ==true, sends the postfix character (last character of ) to the active window. /// public void SendPostfix() { if (this.HasPostfixChar) { var k = new keys(opt.key).AddText(this.Text[^1..], OKeyText.KeysOrChar); try { k.SendNow(); } catch { } //unlikely } //CONSIDER: AddText -> AddChar. Also in other place. But the speed option is different. } /// /// Shows a 1-item menu below the text cursor (caret) or mouse cursor. /// /// Returns true if the user clicked the item or pressed Enter or Tab. /// Text to display. This function limits it to 300 characters. Default: "Replace". /// /// This function is used by when used flag . /// /// The user can close the menu with Enter, Tab or Esc. Other keys close the menu and are passed to the active window. /// /// /// /// /// /// Code in file Autotext triggers. /// o.Replace("Flag Confirm"); /// tt["con2"] = o => { if(o.Confirm("Example")) o.Replace("Function Confirm"); }; /// ]]> /// public bool Confirm(string text = "Replace") { text ??= "Replace"; var m = new popupMenu { RawText = true }; m.Add(1, text.Limit(300)); m.KeyboardHook = (m, g) => { if (g.Key is KKey.Enter or KKey.Tab or KKey.Escape) { if (g.Key != KKey.Escape) m.FocusedItem = m.Items.First(); return PMKHook.Default; } return PMKHook.Close; }; return 1 == _ShowMenu(m); } /// /// Creates and shows a menu below the text cursor (caret) or mouse cursor, and calls . /// /// /// Menu items. An item can be specified as: ///
• string - the replacement text. Also it's the menu item label. ///
- allows to set custom label and the replacement text and/or HTML. ///
null - separator. ///
Label can contain tooltip like "Text\0 Tooltip". /// Replacement text can contain [[|]] to move the caret there (see ). /// /// /// Keyboard: /// - Esc - close the menu. /// - Enter, Tab - select the focused or the first item. /// - Down, Up, End, Home, PageDown, PageUp - focus menu items. /// - Also to select menu items can type the number characters displayed at the right. /// - Other keys close the menu and are passed to the active window. /// /// /// /// /// /// Code in file Autotext triggers. /// o.Menu([ /// "https://www.example.com", /// "[[|]]", /// new("Label example", "TEXT1"), /// null, /// new("HTML example", "TEXT2", "TEXT2"), /// new(null, "TEXT3"), /// ]); /// ]]> /// public void Menu(params TAMenuItem[] items) { if (items.NE_()) return; var m = new popupMenu(null, Trigger.SourceFile, Trigger.SourceLine) { ExtractIconPathFromCode = false, RawText = true, }; for (int i = 0; i < items.Length; i++) { var v = items[i]; if (v == null) { m.Separator(); continue; } string lab = v.Label, text = v.Text ?? v.Html; if (lab == null) { lab = text.Limit(50).RxReplace(@"\R", " "); if (v.Text != null) lab = lab.Replace("[[|]]", null); } #if true var mi = m.Add(i + 1, lab, f_: Trigger.SourceFile, l_: v.l_ > 0 ? v.l_ : Trigger.SourceLine); if (text != lab) mi.Tooltip ??= text; //if (i == 0) m.FocusedItem = mi; //then no tooltip if (i < 9) mi.Hotkey = (i + 1).ToS(); #else //CONSIDER: option "numbers at left side". User suggestion. bool numbersAtLeft = true; string lab2 = numbersAtLeft && i < 9 ? $"{(i + 1).ToS()}. {lab}" : lab; var mi = m.Add(i + 1, lab2, f_: Trigger.sourceFile, l_: v.l_ > 0 ? v.l_ : Trigger.sourceLine); if (text != lab) mi.Tooltip ??= text; //if (i == 0) m.FocusedItem = mi; //then no tooltip if (!numbersAtLeft && i < 9) mi.Hotkey = (i + 1).ToS(); #endif } KeyToTextConverter kt = null; m.KeyboardHook = (m, g) => { if (g.Key is KKey.Enter or KKey.Tab) { m.FocusedItem ??= m.ItemsAndSeparators[0]; return PMKHook.Default; } if (g.Key is KKey.Escape or KKey.Down or KKey.Up or KKey.End or KKey.Home or KKey.PageDown or KKey.PageUp) { return PMKHook.Default; } if (g.Mod != 0) return PMKHook.None; kt ??= new(); if (kt.Convert(out var c, g.vkCode, g.scanCode, keys.getMod(), Window.ThreadId) && c.c is >= '1' and <= '9') { int i = c.c - '1'; if (i >= m.ItemsAndSeparators.Count) return PMKHook.Default; //block m.FocusedItem = m.ItemsAndSeparators[i]; return PMKHook.ExecuteFocused; } return PMKHook.Close; }; int r = _ShowMenu(m) - 1; if (r >= 0) Replace(items[r].Text, items[r].Html); } int _ShowMenu(popupMenu m) { var mo = Trigger.menuOptions; return m.Show(mo?.pmFlags ?? PMFlags.ByCaret); } } /// /// See . /// public class TASimpleReplace { AutotextTriggers _host; internal TASimpleReplace(AutotextTriggers host) { _host = host; } /// /// Adds an autotext trigger. Its action calls . /// /// public string this[string text, TAFlags? flags = null, TAPostfix? postfixType = null, string postfixChars = null, [CallerFilePath] string f_ = null, [CallerLineNumber] int l_ = 0] { set { _host[text, flags, postfixType, postfixChars, f_, l_] = o => o.Replace(value); } } } /// /// Used with . /// public class TAMenuItem { /// public string Label { get; set; } /// public string Text { get; set; } /// public string Html { get; set; } internal int l_; /// /// Sets menu item label and replacement text. /// /// Menu item label. If null, uses . Can contain tooltip like "Label\0 Tooltip". /// The replacement text. Can be null. Can contain caret placeholder [[|]]. See . /// The replacement HTML. Can be null. See . /// [](xref:caller_info) public TAMenuItem(string label, string text, string html = null, [CallerLineNumber] int l_ = 0) { Label = label; Text = text; Html = html; this.l_ = l_; } /// /// Creates TAMenuItem with only . /// public static implicit operator TAMenuItem(string text) => new(null, text, null, 0); } ================================================ FILE: Au/Triggers/Types/t-hotkey.cs ================================================ //CONSIDER: AltGr generates RAlt+LCtrl. Should then ignore Ctrl? namespace Au.Triggers; /// /// Flags of hotkey triggers. /// [Flags] public enum TKFlags { /// /// Allow other apps to receive the key down message too. ///
Without this flag, other apps usually receive only modifier keys. Also, OS always receives Ctrl+Alt+Delete and some other hotkeys. ///
To receive and block key messages is used a low-level hook. Other hooks may receive blocked messages or not, depending on when they were set. ///
ShareEvent = 1, /// /// Run the action when the key and modifier keys are released. /// KeyModUp = 2, /// /// The trigger works only with left-side modifier keys. /// LeftMod = 4, /// /// The trigger works only with right-side modifier keys. /// RightMod = 8, /// /// Don't release modifier keys. ///
Without this flag, for example if trigger is ["Ctrl+K"], when the user presses Ctrl and K down, the trigger sends Ctrl key-up event, making the key logically released, although it is still physically pressed. Then modifier keys don't interfere with the action. However functions like and (and any such functions in any app) will not know that the key is physically pressed; there is no API to get physical key state. ///
Other flags that prevent releasing modifier keys: KeyUp, ShareEvent. Then don't need this flag. ///
Note: Unreleased modifier keys will interfere with mouse functions like . Will not interfere with keyboard and clipboard functions of this library, because they release modifier keys, unless opt.key.NoModOff is true. Will not interfere with functions that send text, unless opt.key.NoModOff is true and opt.key.TextHow is OKeyText.KeysX. ///
NoModOff = 16, /// /// The key must be an "extended key". See example code. Don't use this flag with NumpadX flags. /// ExtendedYes = 32, /// /// The key must not be an "extended key". See example code. Don't use this flag with NumpadX flags. /// ExtendedNo = 64, /// /// The trigger works only with the key that is on the numeric keypad. This flag can be used with Enter, Home, End, PgUp, PgDown, arrows, Ins and Del. /// Numpad = 128, /// /// The trigger works only with the key that is not on the numeric keypad. This flag can be used with Enter, Home, End, PgUp, PgDown, arrows, Ins and Del. /// NumpadNot = 0x100, } /// /// Represents a hotkey trigger. /// public class HotkeyTrigger : ActionTrigger { string _paramsString; internal readonly KMod modMasked, modMask; /// public TKFlags Flags { get; } internal HotkeyTrigger(ActionTriggers triggers, Action action, KMod mod, KMod modAny, TKFlags flags, string paramsString, (string, int) source) : base(triggers, action, true, source) { const KMod csaw = KMod.Ctrl | KMod.Shift | KMod.Alt | KMod.Win; modMask = ~modAny & csaw; modMasked = mod & modMask; Flags = flags; _paramsString = flags == 0 ? paramsString : paramsString + " (" + flags.ToString() + ")"; //print.it(_paramsString); } internal override void Run_(TriggerArgs args) => RunT_(args as HotkeyTriggerArgs); /// /// Returns "Hotkey". /// public override string TypeString => "Hotkey"; /// /// Returns a string containing trigger parameters. /// public override string ParamsString => _paramsString; } /// /// Hotkey triggers. /// /// See . public class HotkeyTriggers : ITriggers, IEnumerable { ActionTriggers _triggers; Dictionary _d = new Dictionary(); internal HotkeyTriggers(ActionTriggers triggers) { _triggers = triggers; } /// /// Adds a hotkey trigger. /// /// /// A hotkey, like with . See [key names and operators](xref:key_names). /// Can contain 0 to 4 modifier keys (Ctrl, Shift, Alt, Win) and 1 non-modifier key. /// Examples: "F11", "Ctrl+K", "Ctrl+Shift+Alt+Win+A". /// To ignore modifiers: "?+K". Then the trigger works with any combination of modifiers. /// To ignore a modifier: "Ctrl?+K". Then the trigger works with or without the modifier. More examples: "Ctrl?+Shift?+K", "Ctrl+Shift?+K". /// /// /// [](xref:caller_info) /// [](xref:caller_info) /// Invalid hotkey string or flags. /// Cannot add triggers after was called, until it returns. /// See . public Action this[[ParamString(PSFormat.HotkeyTrigger)] string hotkey, TKFlags flags = 0, [CallerFilePath] string f_ = null, [CallerLineNumber] int l_ = 0] { set { //This could be used instead of [CallerX] parameters, but can be too slow if many triggers. Definitely too slow for menus and toolbars. //perf.first(); //var uu = new StackFrame(1, true); //first time 30 ms, then 30 mcs //perf.next(); //var us = uu.GetFileName(); //0 //perf.next(); //var ui = uu.GetFileLineNumber(); //0 //perf.next(); //uu = new StackFrame(2, true); //slow too //perf.nw(); ////print.it(us, ui); ////print.it(uu); if (!keys.more.parseTriggerString(hotkey, out var mod, out var modAny, out var key, false)) throw new ArgumentException("Invalid hotkey string."); _Add(value, key, mod, modAny, flags, hotkey, (f_, l_)); } } /// /// Adds a hotkey trigger. /// /// /// /// Modifier keys. See [key names and operators](xref:key_names). /// Examples: "Ctrl", "Ctrl+Shift+Alt+Win". /// To ignore modifiers: "?". Then the trigger works with any combination of modifiers. /// To ignore a modifier: "Ctrl?". Then the trigger works with or without the modifier. More examples: "Ctrl?+Shift?", "Ctrl+Shift?". /// /// /// [](xref:caller_info) /// [](xref:caller_info) /// Invalid modKeys string or flags. /// Cannot add triggers after was called, until it returns. public Action this[KKey key, [ParamString(PSFormat.TriggerMod)] string modKeys, TKFlags flags = 0, [CallerFilePath] string f_ = null, [CallerLineNumber] int l_ = 0] { set { var ps = key.ToString(); if (ps[0].IsAsciiDigit()) ps = "VK" + ps; if (!modKeys.NE()) ps = modKeys + "+" + ps; if (!keys.more.parseTriggerString(modKeys, out var mod, out var modAny, out _, true)) throw new ArgumentException("Invalid modKeys string."); _Add(value, key, mod, modAny, flags, ps, (f_, l_)); } } void _Add(Action action, KKey key, KMod mod, KMod modAny, TKFlags flags, string paramsString, (string, int) source) { if (mod == 0 && flags.HasAny((TKFlags.LeftMod | TKFlags.RightMod))) throw new ArgumentException("Invalid flags."); _triggers.ThrowIfRunning_(); //actually could safely add triggers while running. // Currently would need just lock(_d) in several places. Also some triggers of this type must be added before starting, else we would not have the hook etc. // But probably not so useful. Makes programming more difficult. If need, can Stop, add triggers, then Run again. //print.it($"key={key}, mod={mod}, modAny={modAny}"); var t = new HotkeyTrigger(_triggers, action, mod, modAny, flags, paramsString, source); t.DictAdd_(_d, (int)key); _lastAdded = t; } /// /// The last added trigger. /// public HotkeyTrigger Last => _lastAdded; HotkeyTrigger _lastAdded; bool ITriggers.HasTriggers => _lastAdded != null; void ITriggers.StartStop(bool start) { _UpClear(); _eatUp = 0; } internal bool HookProc(HookData.Keyboard k, TriggerHookContext thc) { //print.it(k.vkCode, !k.IsUp); Debug.Assert(!k.IsInjectedByAu); //server must ignore KKey key = k.vkCode; KMod mod = thc.Mod; bool up = k.IsUp; if (!up) _UpClear(); if (thc.ModThis != 0) { if (_upTrigger != null && mod == 0 && _upKey == 0) _UpTriggered(thc); } else if (up) { if (key == _upKey) { _upKey = 0; if (_upTrigger != null && mod == 0) _UpTriggered(thc); } if (key == _eatUp) { _eatUp = 0; return true; //To be safer, could return false if keys.isPressed(_eatUp), but then can interfere with the trigger action. } //CONSIDER: _upTimeout. } else { //if(key == _eatUp) _eatUp = 0; _eatUp = 0; if (_d.TryGetValue((int)key, out var v)) { HotkeyTriggerArgs args = null; for (; v != null; v = v.next) { var x = v as HotkeyTrigger; if ((mod & x.modMask) != x.modMasked) continue; if ((x.Flags & (TKFlags.Numpad | TKFlags.NumpadNot)) is TKFlags.Numpad or TKFlags.NumpadNot && key is KKey.Enter or KKey.Home or KKey.End or KKey.PageUp or KKey.PageDown or KKey.Left or KKey.Right or KKey.Up or KKey.Down or KKey.Insert or KKey.Delete) { if (k.IsExtended ^ key is KKey.Enter == x.Flags.Has(TKFlags.Numpad)) continue; } switch (x.Flags & (TKFlags.ExtendedYes | TKFlags.ExtendedNo)) { case TKFlags.ExtendedYes: if (!k.IsExtended) continue; break; case TKFlags.ExtendedNo: if (k.IsExtended) continue; break; } switch (x.Flags & (TKFlags.LeftMod | TKFlags.RightMod)) { case TKFlags.LeftMod: if (thc.ModL != mod) continue; break; case TKFlags.RightMod: if (thc.ModR != mod) continue; break; } if (v.DisabledThisOrAll) continue; if (args == null) thc.args = args = new HotkeyTriggerArgs(x, thc.Window, key, mod); //may need for scope callbacks too else args.Trigger = x; if (!x.MatchScopeWindowAndFunc_(thc)) continue; if (x.action != null) { if (0 != (x.Flags & TKFlags.KeyModUp)) { _upTrigger = x; _upArgs = args; _upKey = key; } else { thc.trigger = x; } } //print.it(key, mod); if (0 != (x.Flags & TKFlags.ShareEvent)) return false; if (thc.trigger == null) { //KeyModUp or action==null if (mod is KMod.Alt or KMod.Win or (KMod.Alt | KMod.Win)) { //print.it("need Ctrl"); ThreadPool.QueueUserWorkItem(o => keys.Internal_.SendKey(KKey.Ctrl, dontThrow: true)); //disable Alt/Win menu } } else if (mod != 0) { if (0 == (x.Flags & TKFlags.NoModOff)) thc.muteMod = TriggerActionThreads.c_modRelease; else if (mod is KMod.Alt or KMod.Win or (KMod.Alt | KMod.Win)) thc.muteMod = TriggerActionThreads.c_modCtrl; } _eatUp = key; return true; } } } return false; } HotkeyTrigger _upTrigger; HotkeyTriggerArgs _upArgs; KKey _upKey; KKey _eatUp; void _UpTriggered(TriggerHookContext thc) { thc.args = _upArgs; thc.trigger = _upTrigger; _UpClear(); } void _UpClear() { _upTrigger = null; _upArgs = null; _upKey = 0; } /// /// Used by foreach to enumerate added triggers. /// public IEnumerator GetEnumerator() { foreach (var kv in _d) { for (var v = kv.Value; v != null; v = v.next) { var x = v as HotkeyTrigger; yield return x; } } } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } /// /// Arguments for actions of hotkey triggers. /// public class HotkeyTriggerArgs : TriggerArgs { /// public HotkeyTrigger Trigger { get; internal set; } /// [EditorBrowsable(EditorBrowsableState.Never)] public override ActionTrigger TriggerBase => Trigger; /// /// The active window. /// public wnd Window { get; } /// /// The pressed key. /// public KKey Key { get; } /// /// The pressed modifier keys. /// /// /// Can be useful when the trigger ignores modifiers. For example "?+F11" or "Shift?+A". /// public KMod Mod { get; } /// public HotkeyTriggerArgs(HotkeyTrigger trigger, wnd w, KKey key, KMod mod) { Trigger = trigger; Window = w; Key = key; Mod = mod; } /// public override string ToString() => "Trigger: " + Trigger; } ================================================ FILE: Au/Triggers/Types/t-mouse.cs ================================================ namespace Au.Triggers; /// /// Flags of mouse triggers. /// [Flags] public enum TMFlags : byte { /// /// Allow other apps to receive the mouse button or wheel message too. /// Used only with the click and wheel triggers. /// To receive and block mouse messages is used a low-level hook. Other hooks may receive blocked messages or not, depending on when they were set. /// ShareEvent = 1, /// /// Run the action when the mouse button and modifier keys are released. /// ButtonModUp = 2, /// /// The trigger works only with left-side modifier keys. /// LeftMod = 4, /// /// The trigger works only with right-side modifier keys. /// RightMod = 8, //rejected. We always mod-off and eat auto-repeated and up events with a temp hook. Because OS does not disable auto-repeating like for hotkeys. ///// ///// Don't release modifier keys. ///// More info: . ///// //NoModOff = 16, } /// /// Represents a mouse trigger. /// public class MouseTrigger : ActionTrigger { readonly string _paramsString; readonly byte _data; internal readonly KMod modMasked, modMask; internal MouseTrigger(ActionTriggers triggers, Action action, TMKind kind, byte data, KMod mod, KMod modAny, TMFlags flags, screen screen, string paramsString, (string, int) source) : base(triggers, action, true, source) { const KMod csaw = KMod.Ctrl | KMod.Shift | KMod.Alt | KMod.Win; modMask = ~modAny & csaw; modMasked = mod & modMask; _data = data; _paramsString = paramsString; Flags = flags; Kind = kind; Screen = screen; } internal override void Run_(TriggerArgs args) => RunT_(args as MouseTriggerArgs); /// /// Returns "Mouse". /// public override string TypeString => "Mouse"; /// /// Returns a string containing trigger parameters. /// public override string ParamsString => _paramsString; /// public TMKind Kind { get; } /// public TMClick Button => Kind == TMKind.Click ? (TMClick)_data : default; /// public TMWheel Wheel => Kind == TMKind.Wheel ? (TMWheel)_data : default; /// public TMEdge Edge => Kind == TMKind.Edge ? (TMEdge)_data : default; /// public TMMove Move => Kind == TMKind.Move ? (TMMove)_data : default; /// public screen Screen { get; } /// public TMFlags Flags { get; } } /// /// Mouse triggers. /// /// See . public class MouseTriggers : ITriggers, IEnumerable { ActionTriggers _triggers; Dictionary _d = new Dictionary(); internal MouseTriggers(ActionTriggers triggers) { _triggers = triggers; } /// /// Adds a mouse click trigger. /// /// /// /// Modifier keys. See [key names and operators](xref:key_names). /// Examples: "Ctrl", "Ctrl+Shift+Alt+Win". /// To ignore modifiers: "?". Then the trigger works with any combination of modifiers. /// To ignore a modifier: "Ctrl?". Then the trigger works with or without the modifier. More examples: "Ctrl?+Shift?", "Ctrl+Shift?". /// /// /// [](xref:caller_info) /// [](xref:caller_info) /// Invalid modKeys string or flags. /// Cannot add triggers after was called, until it returns. /// See . public Action this[TMClick button, [ParamString(PSFormat.TriggerMod)] string modKeys = null, TMFlags flags = 0, [CallerFilePath] string f_ = null, [CallerLineNumber] int l_ = 0] { set { _Add(value, TMKind.Click, (byte)button, modKeys, flags, default, button.ToString(), (f_, l_)); } } /// /// Adds a mouse wheel trigger. /// /// public Action this[TMWheel direction, [ParamString(PSFormat.TriggerMod)] string modKeys = null, TMFlags flags = 0, [CallerFilePath] string f_ = null, [CallerLineNumber] int l_ = 0] { set { _Add(value, TMKind.Wheel, (byte)direction, modKeys, flags, default, direction.ToString(), (f_, l_)); } } /// /// Adds a mouse screen edge trigger. /// /// /// The trigger will work in this screen (display monitor). Default: the primary screen. /// Should be lazy or default; else the function calls . /// Examples: screen.at.left(true), screen.index(1, true). /// If screen.ofMouse, the trigger will work in any screen. /// /// [](xref:caller_info) /// public Action this[TMEdge edge, [ParamString(PSFormat.TriggerMod)] string modKeys = null, TMFlags flags = 0, screen screen = default, [CallerFilePath] string f_ = null, [CallerLineNumber] int l_ = 0, [CallerArgumentExpression("screen")] string a1_ = null] { set { _Add(value, TMKind.Edge, (byte)edge, modKeys, flags, screen, edge.ToString(), (f_, l_), a1_); } } /// /// Adds a mouse move trigger. /// /// public Action this[TMMove move, [ParamString(PSFormat.TriggerMod)] string modKeys = null, TMFlags flags = 0, screen screen = default, [CallerFilePath] string f_ = null, [CallerLineNumber] int l_ = 0, [CallerArgumentExpression("screen")] string a1_ = null] { set { _Add(value, TMKind.Move, (byte)move, modKeys, flags, screen, move.ToString(), (f_, l_), a1_); } } MouseTrigger _Add(Action f, TMKind kind, byte data, string modKeys, TMFlags flags, screen screen, string sData, (string, int) source, string a1_ = null) { _triggers.ThrowIfRunning_(); if (screen.Handle != default) print.warning("screen should be lazy or default"); //CONSIDER: store device id or xy, and repair when handle becomes invalid. bool noMod = modKeys.NE(); string ps; using (new StringBuilder_(out var b)) { b.Append(kind.ToString()).Append(' ').Append(sData); if (!noMod) b.Append(" + ").Append(modKeys == "?" ? "any" : modKeys); if (flags != 0) b.Append(" (").Append(flags.ToString()).Append(')'); if (kind is TMKind.Edge or TMKind.Move) { if (screen.IsEmpty) b.Append(", primary screen"); else if (screen.IsOfMouse_) b.Append(", any screen"); else b.Append(", ").Append(a1_); } ps = b.ToString(); //print.it(ps); } KMod mod = 0, modAny = 0; if (noMod) { if (flags.HasAny(kind == TMKind.Click ? TMFlags.LeftMod | TMFlags.RightMod : TMFlags.LeftMod | TMFlags.RightMod | TMFlags.ButtonModUp)) throw new ArgumentException("Invalid flags."); } else { if (!keys.more.parseTriggerString(modKeys, out mod, out modAny, out _, true)) throw new ArgumentException("Invalid modKeys string."); } var t = new MouseTrigger(_triggers, f, kind, data, mod, modAny, flags, screen, ps, source); t.DictAdd_(_d, _DictKey(kind, data)); _lastAdded = t; UsedHookEvents_ |= HooksThread.UsedEvents.Mouse; //just sets the hook UsedHookEvents_ |= kind switch { TMKind.Click => HooksThread.UsedEvents.MouseClick, TMKind.Wheel => HooksThread.UsedEvents.MouseWheel, _ => HooksThread.UsedEvents.MouseEdgeMove, }; return t; } static int _DictKey(TMKind kind, byte data) => (data << 8) | (byte)kind; /// /// The last added trigger. /// public MouseTrigger Last => _lastAdded; MouseTrigger _lastAdded; bool ITriggers.HasTriggers => _lastAdded != null; internal HooksThread.UsedEvents UsedHookEvents_ { get; private set; } void ITriggers.StartStop(bool start) { if (start) { } else { _ResetUpAndUnhookTempKeybHook(); _eatUp = 0; } } internal bool HookProcClickWheel(HookData.Mouse k, TriggerHookContext thc) { //print.it(k.Event, k.pt); Debug.Assert(!k.IsInjectedByAu); //server must ignore TMKind kind; byte data; if (k.IsButton) { if (k.IsButtonUp) { if (k.Event == _upEvent) { _upEvent = 0; if (_upMod == 0 && _upTrigger != null) { thc.args = _upArgs; thc.trigger = _upTrigger; _ResetUp(); } } if (k.Event == _eatUp) { _eatUp = 0; return true; //To be safer, could return false if mouse.isPressed(k.Button), but then can interfere with the trigger action. } return false; //CONSIDER: _upTimeout. } if (k.Event == _eatUp) _eatUp = 0; kind = TMKind.Click; var b = k.Event switch { HookData.MouseEvent.LeftButton => TMClick.Left, HookData.MouseEvent.RightButton => TMClick.Right, HookData.MouseEvent.MiddleButton => TMClick.Middle, HookData.MouseEvent.X1Button => TMClick.X1, _ => TMClick.X2, }; data = (byte)b; } else { //wheel kind = TMKind.Wheel; var b = k.Event switch { HookData.MouseEvent.WheelForward => TMWheel.Forward, HookData.MouseEvent.WheelBackward => TMWheel.Backward, HookData.MouseEvent.WheelLeft => TMWheel.Left, _ => TMWheel.Right, }; data = (byte)b; } return _HookProc2(thc, false, kind, k.Event, k.pt, data, 0); } internal void HookProcEdgeMove(EdgeMoveDetector_.Result d, TriggerHookContext thc) //d not in { TMKind kind; byte data, dataAnyPart; if (d.edgeEvent != 0) { kind = TMKind.Edge; data = (byte)d.edgeEvent; dataAnyPart = (byte)d.edgeEventAnyPart; } else { kind = TMKind.Move; data = (byte)d.moveEvent; dataAnyPart = (byte)d.moveEventAnyPart; } _HookProc2(thc, true, kind, HookData.MouseEvent.Move, d.pt, data, dataAnyPart); } bool _HookProc2(TriggerHookContext thc, bool isEdgeMove, TMKind kind, HookData.MouseEvent mEvent, POINT pt, byte data, byte dataAnyPart) { var mod = TrigUtil.GetModLR(out var modL, out var modR) | _eatMod; MouseTriggerArgs args = null; g1: if (_d.TryGetValue(_DictKey(kind, data), out var v)) { if (!isEdgeMove) thc.UseWndFromPoint(pt); for (; v != null; v = v.next) { var x = v as MouseTrigger; if ((mod & x.modMask) != x.modMasked) continue; switch (x.Flags & (TMFlags.LeftMod | TMFlags.RightMod)) { case TMFlags.LeftMod: if (modL != mod) continue; break; case TMFlags.RightMod: if (modR != mod) continue; break; } if (isEdgeMove && !x.Screen.IsOfMouse_) { var sh = x.Screen.Now; if (!sh.Rect.Contains(pt)) continue; } if (v.DisabledThisOrAll) continue; if (args == null) thc.args = args = new MouseTriggerArgs(x, thc.Window, mod); //may need for scope callbacks too else args.Trigger = x; if (!x.MatchScopeWindowAndFunc_(thc)) continue; if (x.action == null) { _ResetUp(); } else if (0 != (x.Flags & TMFlags.ButtonModUp) && (mod != 0 || kind == TMKind.Click)) { _upTrigger = x; _upArgs = args; _upEvent = kind == TMKind.Click ? mEvent : 0; _upMod = mod; } else { thc.trigger = x; _ResetUp(); } _eatMod = mod; if (mod != 0) { _SetTempKeybHook(); if (thc.trigger != null) thc.muteMod = TriggerActionThreads.c_modRelease; else ThreadPool.QueueUserWorkItem(_ => keys.Internal_.ReleaseModAndDisableModMenu(dontThrow: true)); } //print.it(mEvent, pt, mod); if (isEdgeMove || 0 != (x.Flags & TMFlags.ShareEvent)) return false; if (kind == TMKind.Click) _eatUp = mEvent; return true; } } if (dataAnyPart != 0) { data = dataAnyPart; dataAnyPart = 0; goto g1; } return false; } //these are used to activate trigger when button and modifiers released MouseTrigger _upTrigger; MouseTriggerArgs _upArgs; HookData.MouseEvent _upEvent; KMod _upMod; //this is used to eat modifier keys, regardless when the trigger is activated KMod _eatMod; //these are used to eat modifier keys and to activate trigger when modifiers released WindowsHook _keyHook; long _keyHookTimeout; //this is used to eat button-up event, regardless when the trigger is activated HookData.MouseEvent _eatUp; void _ResetUp() { _upTrigger = null; _upArgs = null; _upEvent = 0; _upMod = 0; } void _UnhookTempKeybHook() { if (_keyHook != null) { //print.it(". unhook"); _keyHook.Unhook(); _keyHookTimeout = _keyHook.DontBlockModInOtherHooks_(0); } } void _ResetUpAndUnhookTempKeybHook() { _ResetUp(); _eatMod = 0; _UnhookTempKeybHook(); } void _SetTempKeybHook() { //print.it(". hook"); if (_keyHook == null) { _keyHook = WindowsHook.Keyboard(k => { if (Environment.TickCount64 >= _keyHookTimeout) { _ResetUpAndUnhookTempKeybHook(); Debug_.Print("hook timeout"); } else { var mod = k.Mod; if (0 != (mod & _upMod) && k.IsUp) { _upMod &= ~mod; if (_upMod == 0 && _upEvent == 0 && _upTrigger != null) { _triggers.RunAction_(_upTrigger, _upArgs); _ResetUp(); } } if (0 != (mod & _eatMod)) { //print.it(k); if (k.IsUp) _eatMod &= ~mod; else k.BlockEvent(); } if (0 == (_upMod | _eatMod)) _UnhookTempKeybHook(); } }, setNow: false); } if (!_keyHook.IsSet) _keyHook.Hook(); _keyHookTimeout = _keyHook.DontBlockModInOtherHooks_(5000); } internal static void JitCompile() { Jit_.Compile(typeof(MouseTriggers), nameof(HookProcClickWheel), nameof(HookProcEdgeMove), nameof(_HookProc2)); wnd.fromXY(default, WXYFlags.NeedWindow); } /// /// Detects trigger events of types Edge and Move. /// /// /// Used in the hook server, to avoid sending all mouse move events to clients, which would use 2 or more times more CPU, eg 0.9% instead of 0.45%. Tested: raw input uses slightly less CPU. /// internal class EdgeMoveDetector_ { int _x, _y; //mouse position. Relative to the primary screen. int _xmin, _ymin, _xmax, _ymax; //min and max possible mouse position in current screen. Relative to the primary screen. _State _prev; int _sens; public Result result; internal struct Result { public POINT pt; public TMEdge edgeEvent, edgeEventAnyPart; public TMMove moveEvent, moveEventAnyPart; } /// /// State data set by previous events. /// struct _State { public int xx, yy; //previous coords public long time; //previous time //these used for Move events public int mx1, my1, mx2, my2; public TMMove mDirection; public long mTimeout; //these used for Edge events public long eTimeout; #if DEBUG //public int debug; #endif } public bool Detect(POINT pt) { //get normal x y. In pt can be outside screen when cursor moved fast and was stopped by a screen edge. Tested: never for click/wheel events. //print.it(pt, mouse.xy); //var scrn = screen.of(pt); //problem with empty corners between 2 unaligned screens: when mouse tries to quickly diagonally cut such a corner, may activate a wrong trigger var scrn = screen.of(mouse.xy); //smaller problem: mouse.xy gets previous coordinates var r = scrn.Rect; _xmin = r.left; _ymin = r.top; _xmax = r.right - 1; _ymax = r.bottom - 1; _x = Math.Clamp(pt.x, _xmin, _xmax); _y = Math.Clamp(pt.y, _ymin, _ymax); //print.it(pt, _x, _y, r); _sens = scrn.Dpi / 4; result = default; result.pt = (_x, _y); _Detect(); _prev.xx = _x; _prev.yy = _y; #if DEBUG //print.it(++_prev.debug, edgeEvent, moveEvent, (_x, _y)); #endif return result.edgeEvent != 0 || result.moveEvent != 0; } void _Detect() { if (mouse.isPressed(MButtons.Left | MButtons.Right | MButtons.Middle)) { _prev.mDirection = 0; return; } int x = _x, y = _y; if (x == _prev.xx && y == _prev.yy) { /*print.it("same x y");*/ return; } long time = perf.ms; int dt = (int)(time - _prev.time); _prev.time = time; if (dt <= 0) return; //never noticed //print.it((x, y), mouse.xy, time%10000); if (y == _ymin || y == _ymax || x == _xmin || x == _xmax) { _prev.mDirection = 0; if (time < _prev.eTimeout) return; //prevent double trigger when OS sometimes gives strange coords if some hook blocks the event if (y == _ymin) { //top if (_prev.yy <= _ymin) return; if (screen.isInAnyScreen((x, y - 1))) return; result.edgeEvent = (TMEdge)((int)(result.edgeEventAnyPart = TMEdge.Top) + _PartX(x)); } else if (y == _ymax) { //bottom if (_prev.yy >= _ymax) return; if (screen.isInAnyScreen((x, y + 1))) return; result.edgeEvent = (TMEdge)((int)(result.edgeEventAnyPart = TMEdge.Bottom) + _PartX(x)); } else if (x == _xmin) { //left if (_prev.xx <= _xmin) return; if (screen.isInAnyScreen((x - 1, y))) return; result.edgeEvent = (TMEdge)((int)(result.edgeEventAnyPart = TMEdge.Left) + _PartY(y)); } else /*if(x == _xmax)*/ { //right if (_prev.xx >= _xmax) return; if (screen.isInAnyScreen((x + 1, y))) return; result.edgeEvent = (TMEdge)((int)(result.edgeEventAnyPart = TMEdge.Right) + _PartY(y)); } _prev.eTimeout = time + 100; } else { if (_prev.mDirection == 0) { if (time < _prev.mTimeout) return; int dx = (x - _prev.xx) * 16 / dt, dy = (y - _prev.yy) * 21 / dt; TMMove e = 0; if (dx > 0) { if (dy > 0) { if (dx > dy) { if (dx > _sens) e = TMMove.RightLeft; } else { if (dy > _sens) e = TMMove.DownUp; } } else { if (dx > -dy) { if (dx > _sens) e = TMMove.RightLeft; } else { if (-dy > _sens) e = TMMove.UpDown; } } } else { if (dy > 0) { if (-dx > dy) { if (-dx > _sens) e = TMMove.LeftRight; } else { if (dy > _sens) e = TMMove.DownUp; } } else { if (-dx > -dy) { if (-dx > _sens) e = TMMove.LeftRight; } else { if (-dy > _sens) e = TMMove.UpDown; } } } if (e != 0) { //print.it(e); _prev.mDirection = e; _prev.mTimeout = time + 250; _prev.mx1 = _prev.xx; _prev.my1 = _prev.yy; _prev.mx2 = x; _prev.my2 = y; } } else { if (time > _prev.mTimeout) { _prev.mDirection = 0; return; } int part = 0; var e = _prev.mDirection; switch (e) { case TMMove.RightLeft: if (x < (_prev.mx1 + _prev.mx2) >> 1) { _prev.mDirection = 0; int dy = (_prev.my1 + _prev.my2) >> 1, dx = (_prev.mx2 - _prev.mx1) >> 2; if (Math.Abs(y - dy) > dx || Math.Abs(_prev.my1 - _prev.my2) > dx << 1) break; part = _PartY(y); } else if (x > _prev.mx2) { _prev.mx2 = x; _prev.my2 = y; } break; case TMMove.LeftRight: if (x > (_prev.mx1 + _prev.mx2) >> 1) { _prev.mDirection = 0; int dy = (_prev.my1 + _prev.my2) >> 1, dx = (_prev.mx1 - _prev.mx2) >> 2; if (Math.Abs(y - dy) > dx || Math.Abs(_prev.my1 - _prev.my2) > dx << 1) break; part = _PartY(y); } else if (x < _prev.mx2) { _prev.mx2 = x; _prev.my2 = y; } break; case TMMove.DownUp: if (y < (_prev.my1 + _prev.my2) >> 1) { _prev.mDirection = 0; int dx = (_prev.mx1 + _prev.mx2) >> 1, dy = (_prev.my2 - _prev.my1) >> 2; if (Math.Abs(x - dx) > dy || Math.Abs(_prev.mx1 - _prev.mx2) > dy << 1) break; part = _PartX(x); } else if (y > _prev.my2) { _prev.mx2 = x; _prev.my2 = y; } break; case TMMove.UpDown: if (y > (_prev.my1 + _prev.my2) >> 1) { _prev.mDirection = 0; int dx = (_prev.mx1 + _prev.mx2) >> 1, dy = (_prev.my1 - _prev.my2) >> 2; if (Math.Abs(x - dx) > dy && Math.Abs(_prev.mx1 - _prev.mx2) > dy << 1) break; part = _PartX(x); } else if (y < _prev.my2) { _prev.mx2 = x; _prev.my2 = y; } break; } if (part != 0) { result.moveEventAnyPart = e; result.moveEvent = (TMMove)((int)e + part); } } } } int _PartX(int x) { int q = (_xmax - _xmin) / 4; if (x < _xmin + q) return 2; if (x >= _xmax - q) return 3; return 1; } int _PartY(int y) { int q = (_ymax - _ymin) / 4; if (y < _ymin + q) return 2; if (y >= _ymax - q) return 3; return 1; } ///// ///// Sensitivity of triggers. ///// Default: 5. Can be 0 (less sensitive) to 10 (more sensitive). ///// //public int MoveSensitivity { // get => _sensPublic; // set { // if((uint)value > 10) throw new ArgumentOutOfRangeException(null, "0-10"); // _sensPublic = value; // _sens = (int)(Math.Pow(1.414, 14 - value) * 1.3); // print.it(_sens); //29 when _sensPublic 5 (default) // } //} //int _sensPublic; } /// /// Used by foreach to enumerate added triggers. /// public IEnumerator GetEnumerator() { foreach (var kv in _d) { for (var v = kv.Value; v != null; v = v.next) { var x = v as MouseTrigger; yield return x; } } } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } /// /// Arguments for actions of mouse triggers. /// public class MouseTriggerArgs : TriggerArgs { /// public MouseTrigger Trigger { get; internal set; } /// [EditorBrowsable(EditorBrowsableState.Never)] public override ActionTrigger TriggerBase => Trigger; /// /// The active window (Edge and Move triggers) or the mouse window (Click and Wheel triggers). /// public wnd Window { get; } /// /// The pressed modifier keys. /// /// /// Can be useful when the trigger ignores modifiers. For example modKeys is "?" or "Shift?". /// public KMod Mod { get; } /// public MouseTriggerArgs(MouseTrigger trigger, wnd w, KMod mod) { Trigger = trigger; Window = w; Mod = mod; } /// public override string ToString() => "Trigger: " + Trigger; } #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member /// /// What kind of mouse triggers. /// public enum TMKind : byte { Click, Wheel, Edge, Move } /// /// Button for mouse click triggers. /// public enum TMClick : byte { Left = 1, Right, Middle, X1, X2 } /// /// Mouse wheel direction for mouse wheel triggers. /// public enum TMWheel : byte { Forward = 1, Backward, Left, Right } /// /// Screen edge for mouse edge triggers. /// /// /// To activate a screen edge trigger, the user touches a screen edge with the mouse pointer. /// Each screen edge is divided into 3 parts: 1 - center 50%; 2 - left or top 25%; 3 - right or bottom 25%. Constants like TopInCenter50 specify an edge and part; the trigger works only in that part of that edge. Constants like Top specify just an edge; the trigger works in all parts of that edge. /// public enum TMEdge : byte { Top = 1, TopInCenter50, TopInLeft25, TopInRight25, Bottom, BottomInCenter50, BottomInLeft25, BottomInRight25, Left, LeftInCenter50, LeftInTop25, LeftInBottom25, Right, RightInCenter50, RightInTop25, RightInBottom25, } /// /// Mouse movement directions for mouse move triggers. /// /// /// To activate a mouse move trigger, the user quickly moves the mouse pointer to the specified direction and back. /// The screen is divided into 3 parts: 1 - center 50%; 2 - left or top 25%; 3 - right or bottom 25%. Constants like UpDownInCenter50 specify a direction and screen part; the trigger works only in that screen part. Constants like UpDown specify just a direction; the trigger works in whole screen. /// public enum TMMove : byte { RightLeft = 1, RightLeftInCenter50, RightLeftInTop25, RightLeftInBottom25, LeftRight, LeftRightInCenter50, LeftRightInTop25, LeftRightInBottom25, UpDown, UpDownInCenter50, UpDownInLeft25, UpDownInRight25, DownUp, DownUpInCenter50, DownUpInLeft25, DownUpInRight25, } //FUTURE: remove. /// [Obsolete("use code like screen.at.left(true) or screen.index(1, true) or screen.ofMouse"), EditorBrowsable(EditorBrowsableState.Never)] public static class TMScreen { public static screen Primary => default; public static screen Any => screen.ofMouse; public static screen OfActiveWindow => screen.ofActiveWindow; public static screen NonPrimary1 => screen.index(1, true); public static screen NonPrimary2 => screen.index(2, true); public static screen NonPrimary3 => screen.index(3, true); public static screen NonPrimary4 => screen.index(4, true); public static screen NonPrimary5 => screen.index(5, true); } #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member ================================================ FILE: Au/Triggers/Types/t-window.cs ================================================ namespace Au.Triggers; /// /// Flags of window triggers. /// [Flags] public enum TWFlags : byte { /// /// Run the action when called, if the window then is active (for ActiveOnce etc triggers) or visible (for VisibleOnce etc triggers). /// RunAtStartup = 1, /// /// When using the later parameter, call the currently active Triggers.FuncOf (see ) functions on "later" events too. /// If the function returns false, the action will not run. /// The function runs synchronously in the same thread that called . The action runs asynchronously in another thread, which is slower to start. /// As always, Triggers.FuncOf functions must not contain slow code; should take less than 10 ms. /// LaterCallFunc = 2, } /// /// Events for window triggers. /// /// /// Cloaked windows are considered invisible. See . /// /// The "active" events don't occur if/while the activated window is invisible. To catch invisible windows too, use a instead of a trigger. See example. /// /// /// How to use a hook instaed of a window trigger. This code can be in the same file as your window triggers. /// { /// print.it(k.w); /// if (k.w.IsMatch("* Name", "classname")) { print.it("fast code can be here", k.w); }; /// //if (k.w.IsMatch("* Name", "classname")) run.thread(() => { print.it("slower code can be here", k.w); }); /// //if (k.w.IsMatch("* Name", "classname")) script.run("large code.cs", k.w.Handle.ToS()); /// }); /// ]]> /// public enum TWEvent { /// /// When the specified window becomes active (each time). /// Active, /// /// When the specified window becomes active the first time in the trigger's life. /// ActiveOnce, /// /// When the specified window is created and then becomes active. /// The same as , but windows created before calling are ignored. /// ActiveNew, /// /// When the specified window becomes visible (each time). /// Visible, /// /// When the specified window becomes visible the first time in the trigger's life. /// VisibleOnce, /// /// When the specified window is created and then becomes visible. /// The same as , but windows created before calling are ignored. /// VisibleNew, } /// /// Window events for the later parameter of window triggers. See . /// [Flags] public enum TWLater : ushort { /// /// Name changed. /// This event occurs only when the window is active. If name changed while inactive - when afterwards activated. /// Name = 1, /// /// Destroyed (closed). /// Destroyed = 2, /// /// Activated (became the foreground window). /// Active = 4, /// /// Deactivated (lose the foreground window status). /// This event also occurs when closing the window, if it was active; then the window possibly is already destroyed, and the handle is invalid. /// Inactive = 8, /// /// Became visible ( true). /// The window can be new or was temporarily hidden. /// The window is not actually visible if cloaked, minimized, etc. /// Visible = 16, /// /// Became invisible ( false). /// This event also occurs when closing the window, if it was visible; then the window possibly is already destroyed, and the handle is invalid. /// Invisible = 32, /// /// The window has been cloaked ( true). /// Cloaked = 64, /// /// The window has been uncloaked ( false). /// Uncloaked = 128, /// /// The window has been minimized. /// Minimized = 0x100, /// /// The window has been restored from the minimized state. /// Unminimized = 0x200, //rejected. Not useful when we don't have notifications for each location change. ///// ///// The user started to move or resize the window. ///// This event does not occur when maximizing/restoring and when the window is moved/resized not by the user. ///// //MoveSizeStart = 0x400, ///// ///// The user finished (or canceled) to move or resize the window. ///// This event does not occur when maximizing/restoring and when the window is moved/resized not by the user. ///// //MoveSizeEnd = 0x800, //rejected: Location. Use timer or thread-specific EEvent.OBJECT_LOCATIONCHANGE. // Probably it should not be a trigger. If timer, too slow in many cases. If hook, too frequent trigger when drag-moving etc. // If a script wants to track window location, it can easily set WinEventHook(EEvent.OBJECT_LOCATIONCHANGE) instead. //rejected: Focus (when eg a child control focused). Use timer or EEvent.OBJECT_FOCUSED. // Rarely used. A script can easily use WinEventHook(EEvent.OBJECT_FOCUSED). //rejected: Timer. } /// /// Represents a window trigger. /// /// /// print.it(o.Window); /// var v = Triggers.Window.Last; //v is the new WindowTrigger. Rarely used. /// ]]> /// public class WindowTrigger : ActionTrigger { string _typeString, _paramsString; /// public TWEvent Event { get; } /// public wndFinder Finder { get; } /// public TWFlags Flags { get; } /// public TWLater Later { get; } internal WindowTrigger(ActionTriggers triggers, Action action, TWEvent ev, wndFinder finder, TWFlags flags, TWLater later, (string, int) source) : base(triggers, action, false, source) { this.Event = ev; this.Finder = finder; this.Flags = flags; this.Later = later; } internal override void Run_(TriggerArgs args) => RunT_(args as WindowTriggerArgs); /// /// Returns a trigger type string, like "Window.ActiveNew". /// public override string TypeString => _typeString ??= "Window." + Event; /// /// Returns a string containing trigger parameters. /// public override string ParamsString => _paramsString ??= Finder.ToString(); internal bool IsVisible => Event >= TWEvent.Visible; internal bool IsOnce => Event is TWEvent.ActiveOnce or TWEvent.VisibleOnce; internal bool IsNew => Event is TWEvent.ActiveNew or TWEvent.VisibleNew; internal bool IsAlways => Event is TWEvent.Active or TWEvent.Visible; } /// /// Window triggers. /// /// /// Note: the Triggers in examples is a field or property like readonly ActionTriggers Triggers = new();. /// print.it(o.Window); /// wt[TWEvent.Visible, "Window2 name"] = o => print.it(o.Window); /// Triggers.Run(); /// ]]> /// More examples: . /// public class WindowTriggers : ITriggers, IEnumerable { ActionTriggers _triggers; internal WindowTriggers(ActionTriggers triggers) { _triggers = triggers; } /// /// Adds a window trigger and its action. /// /// Trigger event. /// Trigger flags. /// /// Can optionally specify one or more additional events. /// This starts to work when the primary trigger is activated, and works only for that window. /// For example, to be notified when the window is closed or renamed, specify later: TWLater.Destroyed | TWLater.Name. /// When a "later" event occurs, the trigger action is executed. The property then is that event; it is 0 when it is the primary trigger. /// The "later" triggers are not disabled when primary triggers are disabled. /// /// [](xref:caller_info) /// [](xref:caller_info) /// Cannot add triggers after was called, until it returns. /// See . /// /// public Action this[TWEvent winEvent, [ParamString(PSFormat.Wildex)] string name = null, [ParamString(PSFormat.Wildex)] string cn = null, [ParamString(PSFormat.Wildex)] WOwner of = default, Func also = null, WContains contains = default, TWFlags flags = 0, TWLater later = 0, [CallerFilePath] string f_ = null, [CallerLineNumber] int l_ = 0 ] { set { var f = new wndFinder(name, cn, of, 0, also, contains); this[winEvent, f, flags, later, f_, l_] = value; } } /// /// Adds a window trigger and its action. /// /// Cannot add triggers after was called, until it returns. /// public Action this[TWEvent winEvent, wndFinder f, TWFlags flags = 0, TWLater later = 0, [CallerFilePath] string f_ = null, [CallerLineNumber] int l_ = 0] { set { _triggers.ThrowIfRunning_(); if (f.Props.contains.Value is uiimageFinder) print.warning("Window triggers with 'contains image' are unreliable."); else if (f.Props.contains.Value is ocrFinder) print.warning("Window triggers with 'contains OCR text' are slow."); var t = new WindowTrigger(_triggers, value, winEvent, f, flags, later, (f_, l_)); ref var last = ref _tActive; if (t.IsVisible) last = ref _tVisible; if (last == null) { last = t; last.next = last; } else { t.next = last.next; //first last.next = t; last = t; } _laterEvents |= later; _lastAdded = t; } } /// /// The last added trigger. /// public WindowTrigger Last => _lastAdded; WindowTrigger _lastAdded; bool ITriggers.HasTriggers => _lastAdded != null; WindowTrigger _tActive, _tVisible; //null or last trigger in linked list TWLater _laterEvents, _allEvents; bool _usesVisibleArray; //0 != (_allEvents & (TWLater.Visible | TWLater.Invisible)) unsafe void ITriggers.StartStop(bool start) { if (start) { if (_enumWinProc == null) { _enumWinProc = _EnumWinProc; _aTriggered.a = new wnd[100]; _aTriggeredData = new _TriggeredData[100]; _aVisible.a = new wnd[100]; _aVisibleOld.a = new wnd[100]; _hsOld = new HashSet(1000); _hsSeenActivating = new HashSet(50); _winPropCache = new WFCache { CacheName = true, NoTimeout = true, IgnoreVisibility = true }; } else { Array.Clear(_aTriggeredData, 0, _aTriggered.len); _aTriggered.len = _aVisible.len = 0; _hsOld.Clear(); _hsSeenActivating.Clear(); _winPropCache.Clear(); } _allEvents = _laterEvents | TWLater.Active | TWLater.Name; if (_tVisible != null) _allEvents |= TWLater.Visible | TWLater.Uncloaked; _usesVisibleArray = 0 != (_allEvents & (TWLater.Visible | TWLater.Invisible)); var ah = new EEvent[5]; ah[0] = EEvent.SYSTEM_FOREGROUND; if (osVersion.minWin8) { if (0 != (_allEvents & TWLater.Uncloaked)) ah[1] = EEvent.OBJECT_UNCLOAKED; if (0 != (_allEvents & TWLater.Cloaked)) ah[2] = EEvent.OBJECT_CLOAKED; } if (0 != (_allEvents & TWLater.Minimized)) ah[3] = EEvent.SYSTEM_MINIMIZESTART; if (0 != (_allEvents & TWLater.Unminimized)) ah[4] = EEvent.SYSTEM_MINIMIZEEND; //if(0 != (_allEvents & TWLater.MoveSizeStart)) ah[5] = EEvent.SYSTEM_MOVESIZESTART; //if(0 != (_allEvents & TWLater.MoveSizeEnd)) ah[6] = EEvent.SYSTEM_MOVESIZEEND; _hooks = new WinEventHook(ah, _HookProc); _hookEventQueue = new Queue<(EEvent, int)>(); _triggers.WinTimerPeriod_ = 250; Api.IVirtualDesktopManager dm = null; Api.EnumWindows((w, param) => { if (w.IsVisible) { if (_usesVisibleArray) _aVisible.Add(w); //skip empty winstore hosts that later probably will be used as new windows. Speed: ~100 mcs, first time ~10 ms. if (osVersion.minWin10 && w.HasExStyle(WSE.NOREDIRECTIONBITMAP) && w.IsCloaked && w.ClassNameIs("ApplicationFrameWindow")) { //is it a window in an inactive virtual desktop? In both cases it does not have a child Windows.UI.Core.CoreWindow. dm ??= new Api.VirtualDesktopManager() as Api.IVirtualDesktopManager; if (0 == dm.GetWindowDesktopId(w.Get.RootOwnerOrThis(), out var guid) && guid == default) { //print.it(w); return 1; } } } _hsOld.Add(w); return 1; }); Api.ReleaseComObject(dm); _wActive = default; _nameActive = null; //run trigers that have flag TWFlags.RunAtStartup for (int i = 0; i < _aVisible.len; i++) _Proc(TWLater.Visible, _aVisible.a[i], _ProcCaller.Startup); var wa = wnd.active; if (!wa.Is0) _Proc(TWLater.Active, wa, _ProcCaller.Startup); } else { _hooks?.Dispose(); _hooks = null; _hookEventQueue = null; } } WinEventHook _hooks; Queue<(EEvent, int)> _hookEventQueue; WFCache _winPropCache; struct _TriggeredData { public object triggered; //WindowTrigger or List public string name; //for Name triggers } _WndArray _aTriggered, _aVisible, _aVisibleOld; _TriggeredData[] _aTriggeredData; HashSet _hs1, _hs2; //to find added and removed visible windows HashSet _hsOld, _hsSeenActivating; wnd _wActive; string _nameActive; int _timerCounter10; /// /// Called from the message loop every 250 or less ms. /// internal unsafe void Timer_() { //bool print = !keys.isNumLock; //if(print) print.it(perf.ms % 10000); int period = _triggers.WinTimerPeriod_; if (period < 250) _triggers.WinTimerPeriod_ = Math.Min(period += period / 10 + 1, 250); //bool verbose = !keys.isNumLock; //if(keys.isNumLock) return; //perf.first(); //var a = wnd.getwnd.allWindows(true); //wnd.getwnd.allWindows(ref _listVisible, true); //Array.Sort(a); //using(var a=wnd.Internal_.EnumWindows2(wnd.Internal_.EnumAPI.EnumWindows, true)) { //} //if(_aVisibleOld != null) { // //var added = a.Except(_aVisibleOld); // //var removed = _aVisibleOld.Except(a); // //int n1 = added.Count(), n2 = removed.Count(); // //if(n1 + n2 > 0) print.it(n1, n2); //} //_aVisibleOld = a; //Debug_.MemoryPrint_(); //bool needEW = false; //if(Visible.HasTriggers) needEW = true; //else if(Active.HasTriggers) { if(++_timerCounterActiveEW >= 20) { _timerCounterActiveEW = 0; needEW = true; } } if (_usesVisibleArray) { //print.it(perf.ms % 10000); var t1 = _aVisibleOld; _aVisibleOld = _aVisible; _aVisible = t1; _aVisible.len = 0; Api.EnumWindows(_enumWinProc); //perf.next(); _VisibleAddedRemoved(); //perf.nw(); //speed with 3 main windows: 200, +IsVisible 270, +IsCloaked 450, +Name-IsCloaked 444 //speed with 20 main windows (79 visible, 541 total): 320, +IsVisible 430 (CPU 0.05). With 480 CPU 0.06, sometimes 0.05. //print.it(n); //print.it(_listVisible.Count); } var a = _aTriggered.a; for (int i = 0; i < _aTriggered.len; i++) { if (!Api.IsWindow(a[i])) { _ProcLater(TWLater.Destroyed, a[i], i); int last = --_aTriggered.len; a[i] = a[last]; _aTriggeredData[i] = _aTriggeredData[last]; _aTriggeredData[last] = default; } } if (++_timerCounter10 >= 10) { //every 2.5 s _timerCounter10 = 0; _hsOld.RemoveWhere(o => !Api.IsWindow(o)); _hsSeenActivating.RemoveWhere(o => !Api.IsWindow(o)); _winPropCache.Clear(); } var w = wnd.active; if (w.Is0) { if (!_wActive.Is0) _ProcLater(TWLater.Inactive, _wActive); _wActive = default; _nameActive = null; } else if (w != _wActive) { _Proc(TWLater.Active, w); } else { var name = w.NameTL_; if (name != null && name != _nameActive) { _nameActive = name; _Proc(TWLater.Name, w, name: name); } } } /// /// Callback of EnumWindows used by Timer_ to get visible windows. /// Api.WNDENUMPROC _enumWinProc; unsafe int _EnumWinProc(wnd w, void* _) { if (w.IsVisible) _aVisible.Add(w); return 1; } /// /// Called by Timer_ when it swaps _aVisible with _aVisibleOld and calls EnumWindows to populate _aVisible with visible windows. /// Finds what windows became visible or invisible and runs Visible/Invisible triggers for them. /// void _VisibleAddedRemoved() { //perf.first(); wnd[] aNew = _aVisible.a, aOld = _aVisibleOld.a; int nNew = _aVisible.len, nOld = _aVisibleOld.len; int to = Math.Min(nOld, nNew), diffFrom; for (diffFrom = 0; diffFrom < to; diffFrom++) if (aNew[diffFrom] != aOld[diffFrom]) break; if (nNew == nOld && diffFrom == nNew) return; int diffTo1 = nOld, diffTo2 = nNew; while (diffTo1 > 0 && diffTo2 > 0) if (aNew[--diffTo2] != aOld[--diffTo1]) { diffTo1++; diffTo2++; break; } //print.it(diffFrom, diffTo1, diffTo2, Math.Max(diffTo1 - diffFrom, diffTo2 - diffFrom)); int n1 = diffTo1 - diffFrom, n2 = diffTo2 - diffFrom; //print.it($"from={diffFrom} to1={diffTo1} to2={diffTo2} n1={n1} n2={n2} n1*n2={n1*n2}"); if (n1 == 0) { //only added if (0 != (_allEvents & TWLater.Visible)) { for (int i = diffFrom; i < diffTo2; i++) _Added(i); } } else if (n2 == 0) { //only removed if (0 != (_allEvents & TWLater.Invisible)) { for (int i = diffFrom; i < diffTo1; i++) _Removed(i); } } else { //reordered or/and added/removed //print.it($"n1={n1} n2={n2} n1*n2={n1 * n2}"); //perf.next(); //TODO3: optimize. Now slow when large array reordered. Eg divide the changed range into two. //n1 = n2 = 0; if (0 != (_allEvents & TWLater.Invisible)) { if (_hs2 == null) _hs2 = new HashSet(500); else _hs2.Clear(); for (int i = diffFrom; i < diffTo2; i++) _hs2.Add(aNew[i]); for (int i = diffFrom; i < diffTo1; i++) if (!_hs2.Remove(aOld[i])) _Removed(i); } if (0 != (_allEvents & TWLater.Visible)) { if (_hs1 == null) _hs1 = new HashSet(500); else _hs1.Clear(); for (int i = diffFrom; i < diffTo1; i++) _hs1.Add(aOld[i]); for (int i = diffFrom; i < diffTo2; i++) if (!_hs1.Remove(aNew[i])) _Added(i); } //perf.nw(); } //if(n1 + n2 > 0) print.it($"<>added {n2}, removed {n1}<>"); void _Added(int i) { //n2++; //print.it("added", aNew[i]); _Proc(TWLater.Visible, aNew[i]); } void _Removed(int i) { //n1++; //print.it("removed", aOld[i]); _ProcLater(TWLater.Invisible, aOld[i]); } } /// /// WinEventHook hook procedure. /// /// void _HookProc(HookData.WinEvent k) { //if(!keys.isNumLock) print.it(k.event_, k.idObject, k.idChild, k.w); if (k.idObject != EObjid.WINDOW) return; if (k.idChild != 0 || k.w.Is0) return; var event_ = k.event_; var w = k.w; if (w.IsChild) { //if(event_ == EEvent.OBJECT_UNCLOAKED) { } //rejected: wait for winstore app host child window return; } if (_inProc) { _hookEventQueue.Enqueue((event_, (int)w)); //print.it(_hookEventQueue.Count); Debug_.PrintIf(_hookEventQueue.Count > 4, "_hookEventQueue.Count=" + _hookEventQueue.Count); return; } _inProc = true; try { //wait.doEvents(300); //test queue //perf.cpu(); for (; ; ) { TWLater e = 0; switch (event_) { case EEvent.SYSTEM_FOREGROUND: //Debug_.PrintIf(!w.IsActive, $"{perf.ms % 10000}, SYSTEM_FOREGROUND but not active: {w}"); //it is normal. The window either will be active soon (and timer will catch it), or will not be activated (eg prevented by Windows, hooks, etc), or another window became active. if (w != _wActive && w.IsActive) e = TWLater.Active; break; case EEvent.OBJECT_UNCLOAKED: e = TWLater.Uncloaked; break; case EEvent.OBJECT_CLOAKED: e = TWLater.Cloaked; break; case EEvent.SYSTEM_MINIMIZESTART: e = TWLater.Minimized; break; case EEvent.SYSTEM_MINIMIZEEND: e = TWLater.Unminimized; break; //case EEvent.SYSTEM_MOVESIZESTART: e = TWLater.MoveSizeStart; break; //case EEvent.SYSTEM_MOVESIZEEND: e = TWLater.MoveSizeEnd; break; } if (e != 0) _Proc(e, w, _ProcCaller.Hook); //Normal timer period is 255. It is optimized for quite small CPU usage and quite good response time. //To improve response time, we temporarily set smaller period on SYSTEM_FOREGROUND. Need it when the window is not active too. //If activating a new window first time, set period = 15, to quickly catch Visible and Name events. //Else set period = 105, for Name events after closing an "Open File" dialog (then usually its owner becomes active and then changes name). if (event_ == EEvent.SYSTEM_FOREGROUND) { bool fast = _hsSeenActivating.Add(w) && !_hsOld.Contains(w); //print.it("fast timer", fast); _triggers.WinTimerPeriod_ = fast ? 1 : 100; } if (_hookEventQueue.Count == 0) break; var q = _hookEventQueue.Dequeue(); event_ = q.Item1; w = (wnd)q.Item2; } _inProc = false; } finally { if (_inProc) { _inProc = false; _hookEventQueue.Clear(); } //exception. Unlikely, because we handle script exceptions, except thread abort. } } bool _inProc; enum _ProcCaller { Timer, Hook, Run, Startup } /// /// Processes events for main triggers (active, visible) and most "later" triggers. /// Called from hook (_HookProc), timer (Timer_), at startup (StartStop) and SimulateActiveNew/SimulateVisibleNew. /// void _Proc(TWLater e, wnd w, _ProcCaller caller = _ProcCaller.Timer, string name = null) { //e can be: //Used for main triggers (Active, Visible) and other triggers: // Active - from SYSTEM_FOREGROUND hook or from timer. Now the window is active and not 0. // Name - from timer, and only when the window is active. // Visible - from timer. // Uncloaked - from OBJECT_UNCLOAKED hook. //Used only for other triggers: // Cloaked - from OBJECT_CLOAKED hook. // Minimized from SYSTEM_MINIMIZESTART hook. // Unminimized from SYSTEM_MINIMIZEEND hook. // rejected: MoveSizeStart, MoveSizeEnd. Debug.Assert(e == TWLater.Active || e == TWLater.Name || e == TWLater.Visible || e == TWLater.Uncloaked || e == TWLater.Cloaked || e == TWLater.Minimized || e == TWLater.Unminimized/* || e == TWLater.MoveSizeStart || e == TWLater.MoveSizeEnd*/); bool callerHT = caller == _ProcCaller.Hook || caller == _ProcCaller.Timer; //Ignore Active if invisible. Triggering while invisible creates more problems than is useful. The timer will call us later. // It makes the trigger slower in many cases. Usually the speed is important when closing unwanted messageboxes etc. // If closed while invisible, the user usually never sees the window. It seems like it would be better, but can create problems: // 1. If the trigger uses 'contains', in some windows we cannot find the object because it is still invisible etc; example - TaskDialog windows. // 2. If the trigger is not perfectly specified and its action closes wrong windows, the user does not know what is going on. // 3. It temporarily deactivates the current window anyway, and often the user is unaware about it. if (e == TWLater.Active && callerHT && !w.IsVisibleAndNotCloaked) return; //if(e == TWLater.Active && callerHT) { if(!w.IsVisibleAndNotCloaked) { _perf.First(); return; } else if(caller== _ProcCaller.Timer) _perf.NW('A'); } int iTriggered = _aTriggered.Find(w); bool detectedVisibleNow = e != TWLater.Visible && _usesVisibleArray && callerHT && w.IsVisible && _aVisible.Find(w) < 0; if (detectedVisibleNow) { _aVisible.Add(w); _ProcLater(TWLater.Visible, w, iTriggered); } bool runActive = false; if (e == TWLater.Active) { name = w.NameTL_; if (callerHT) { _ProcLater(TWLater.Name, w, iTriggered, name); //maybe name changed while inactive if (!_wActive.Is0) _ProcLater(TWLater.Inactive, _wActive); } _wActive = w; _nameActive = name; if (callerHT) _ProcLater(e, w, iTriggered); runActive = _tActive != null; } else { if (callerHT) _ProcLater(e, w, iTriggered, name); switch (e) { case TWLater.Name: //from timer, when active window name changed runActive = _tActive != null; break; case TWLater.Visible: case TWLater.Uncloaked: name = w.NameTL_; break; default: return; } } bool runVisible = _tVisible != null && w.IsVisibleAndNotCloaked; if (!(runVisible | runActive)) return; bool oldWindow = callerHT && _hsOld.Contains(w); if (_log && callerHT && (_logSkip == null || !_logSkip(w))) { if (detectedVisibleNow) _LogEvent(TWLater.Visible, w, caller, oldWindow); _LogEvent(e, w, caller, oldWindow); } object triggered = iTriggered >= 0 ? _aTriggeredData[iTriggered].triggered : null, triggeredOld = triggered; _winPropCache.Begin(w); //info: don't need to call Clear now. Timer calls it every 2.5 s. _winPropCache.Name = name; //perf.first(); WindowTriggerArgs args = null; //print.it(runVisible,runActive,w.IsActive, oldWindow); if (runVisible) _Do(true, runActive && !detectedVisibleNow); if (runActive && w.IsActive) _Do(false, e != TWLater.Active); void _Do(bool visible, bool secondaryEvent) { ActionTrigger last = visible ? _tVisible : _tActive, v = last; if (last == null) return; do { v = v.next; if (v.DisabledThisOrAll) continue; var t = v as WindowTrigger; if (caller == _ProcCaller.Startup && 0 == (t.Flags & TWFlags.RunAtStartup)) continue; if (oldWindow && (t.IsNew || secondaryEvent)) continue; if (triggered != null && (!t.IsAlways || secondaryEvent)) { switch (triggered) { case WindowTrigger wt1: if (wt1 == t) continue; break; case List awt1: if (awt1.Contains(t)) continue; break; } } try { if (!t.Finder.IsMatch(w, _winPropCache)) continue; //rejected: if 'contains' is image and it does not match, wait several seconds until it matches. // Now such triggers sometimes don't work because need more time to show the image. // Rare and too difficult. Cannot wait now. Would need to use timer or threadpool. } catch (Exception ex) { print.it(ex); continue; } if (args == null) args = new WindowTriggerArgs(t, w, 0); else args.Trigger = t; if (!t.CallFunc_(args)) continue; //info: handles exceptions switch (triggered) { case null: triggered = t; break; case WindowTrigger wt1: if (wt1 != t) triggered = new List { wt1, t }; break; case List awt1: if (!awt1.Contains(t)) awt1.Add(t); break; } //if(!visible && !w.IsActive) break; //no if (_log) print.it($"<>{t.TypeString}<>"); _triggers.RunAction_(t, args); } while (v != last); //perf.nw(); //speed ms/triggers when cold CPU: ~1/1000, 10/10000, 50/100000, 130/1000000 } if (triggered != triggeredOld) { if (iTriggered < 0) { iTriggered = _aTriggered.len; _aTriggered.Add(w); if (iTriggered == _aTriggeredData.Length) Array.Resize(ref _aTriggeredData, iTriggered * 2); _aTriggeredData[iTriggered].name = name; //later managed by _Proc2 } _aTriggeredData[iTriggered].triggered = triggered; } } /// /// Called to process "later" events from _Proc and timer. /// iTriggered is w index in _aTriggered, or -1 if not found, or -2 (default) to let this func find. /// void _ProcLater(TWLater e, wnd w, int iTriggered = -2, string name = null) { if (0 == (_laterEvents & e)) return; if (iTriggered < -1) iTriggered = _aTriggered.Find(w); if (iTriggered < 0) return; if (e == TWLater.Name) { //called in 2 cases: 1. From timer, when name changed. 2. From _Proc on Active event (hook or timer). if (name == null || _aTriggeredData[iTriggered].name == name) return; _aTriggeredData[iTriggered].name = name; } if (_log) print.it($"\t{perf.ms % 10000,4}, {e,-11}, {w}"); WindowTriggerArgs args = null; var triggered = _aTriggeredData[iTriggered].triggered; var a = triggered as List; int n = a?.Count ?? 1; for (int i = 0; i < n; i++) { var t = a?[i] ?? (triggered as WindowTrigger); if (0 == (t.Later & e)) continue; //if(t.DisabledThisOrAll) continue; //no if (args == null) args = new WindowTriggerArgs(t, w, e); else args.Trigger = t; if (0 != (t.Flags & TWFlags.LaterCallFunc)) { if (!t.CallFunc_(args)) continue; } if (_log) print.it($"<>\t{e}<>"); _triggers.RunAction_(t, args); } } /// /// Simulates event "activated new window" as if the specified window is that window. /// /// Called before or after . /// /// This function usually is used to run ActiveNew triggers for a window created before calling . Here "run triggers" means "compare window properties etc with those specified in triggers and run actions of triggers that match". Normally such triggers don't run because the window is considered old. This function runs triggers as it was a new window. Triggers like ActiveNew and ActiveOnce will run once, as usually. /// This function must be called while the main triggers thread is in , for example from another trigger action. It is asynchronous (does not wait). /// If called from a trigger action (hotkey etc), make sure the window trigger action runs in another thread or can be queued. Else both actions cannot run simultaneously. See example. /// /// /// Note: the Triggers in examples is a field or property of type . /// o.Window.Resize(500, 200); /// Triggers.Hotkey["Ctrl+T"] = o => Triggers.Window.SimulateActiveNew(wnd.active); /// ]]> /// public void SimulateActiveNew(wnd w) => _SimulateNew(TWLater.Active, w); /// /// Simulates event "visible new window" as if the specified window is that window. /// Similar to . /// /// /// Called before or after . /// /// Cannot be called before or after . /// public void SimulateVisibleNew(wnd w) => _SimulateNew(TWLater.Visible, w); void _SimulateNew(TWLater e, wnd w) { _triggers.ThrowIfNotRunning_(); _triggers.SendMsg_(false, () => { if (w.IsAlive) _Proc(e, w, _ProcCaller.Run); }); } bool _log; Func _logSkip; /// /// Starts or stops to log (write in output) window events that can help to create or debug window triggers. /// /// Start (true) or stop. /// An optional callback function that can be used to reduce noise, eg skip tooltip windows. Return true to skip that window. /// /// For primary trigger events is logged this info: ///
    ///
  1. Time milliseconds. Shows only the remainder of dividing by 10 seconds, therefore it starts from 0 again when reached 9999 (9 seconds and 999 milliseconds).
  2. ///
  3. Event (see ).
  4. ///
  5. Letters for window state etc: ///
      ///
    • A - the window is active.
    • ///
    • H - the window is invisible (!).
    • ///
    • C - the window is cloaked ().
    • ///
    • O - the window is considered old, ie created before calling .
    • ///
    • T - the even has been detected using a timer, which means slower response time. Else detected using a hook.
    • ///
    ///
  6. ///
  7. Window (handle, class, name, program, rectangle).
  8. ///
/// /// Colors are used for window event types used for primary triggers: blue if activated; green if became visible; yellow if name changed. /// For "later" events is logged time, event and window. Black, tab-indented. Only events that are specified in triggers. /// When a trigger is activated, the event type is red. ///
/// /// 0 != o.ClassNameIs("*tooltip*", "SysShadow", "#32774", "TaskList*")); /// ]]> /// public void LogEvents(bool on, Func skip = null) { _log = on; _logSkip = skip; } /// /// Called by _Proc. /// void _LogEvent(TWLater e, wnd w, _ProcCaller caller, bool oldWindow) { string col = "0"; switch (e) { case TWLater.Active: col = "0x0000ff"; break; case TWLater.Visible: case TWLater.Uncloaked: col = "0x00c000"; break; case TWLater.Name: col = "0xC0C000"; break; } var A = w.IsActive ? "A" : " "; var H = w.IsVisible ? " " : "H"; var C = w.IsCloaked ? "C" : " "; var O = oldWindow ? "O" : " "; var T = caller == _ProcCaller.Timer ? "T" : " "; print.it($"<>{perf.ms % 10000,4}, {e,-11}, {A}{H}{C}{O}{T}, {w}"); } /// /// For _aTriggered, _aVisible and _aVisibleOld we use resizable array, not List. /// We access elements by index in time-critical code. With List it is much slower. /// struct _WndArray { public wnd[] a; public int len; public void Add(wnd w) { if (len == a.Length) Array.Resize(ref a, len * 2); a[len++] = w; } public unsafe int Find(wnd w) { fixed (wnd* p = a) { for (int i = 0, n = len; i < n; i++) if (p[i] == w) return i; } return -1; } } /// /// Used by foreach to enumerate added triggers. /// public IEnumerator GetEnumerator() { ActionTrigger last, v; if (_tActive != null) { last = v = _tActive; do { v = v.next; var x = v as WindowTrigger; yield return x; } while (v != last); } if (_tVisible != null) { last = v = _tVisible; do { v = v.next; var x = v as WindowTrigger; yield return x; } while (v != last); } } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } /// /// Arguments for actions of window triggers. /// public class WindowTriggerArgs : TriggerArgs { /// /// The trigger. /// public WindowTrigger Trigger { get; internal set; } /// [EditorBrowsable(EditorBrowsableState.Never)] public override ActionTrigger TriggerBase => Trigger; /// /// The window. /// public wnd Window { get; } /// /// The "later" event, or 0 if it is the primary trigger (ActiveNew etc). See example. /// /// /// Note: the Triggers in examples is a field or property like readonly ActionTriggers Triggers = new();. /// print.it(o.Later, o.Window); /// Triggers.Run(); /// ]]> /// public TWLater Later { get; } /// public WindowTriggerArgs(WindowTrigger trigger, wnd w, TWLater later) { Trigger = trigger; Window = w; Later = later; } /// public override string ToString() => "Trigger: " + Trigger; /// /// Shows or hides a window-attached toolbar depending on window name. /// Use in window trigger action, like in examples. /// /// Toolbar function. Let it create a window-attached toolbar and returns the object. /// Window name when the toolbar should be visible. String format: [wildcard expression](xref:wildcard_expression). /// Invalid wildcard expression ("**options " or regular expression). /// /// The trigger must have argument , later: TWLater.Name (see ) and match the window with any name. /// /// /// Show toolbar Toolbar_Chrome1 on a Chrome window when window name starts with "NuGet". /// ta.ShowToolbarWhenWindowName(Toolbar_Chrome1, "NuGet*"); /// ]]> /// Toolbar Toolbar_Chrome1 for the above example. /// /// Show Toolbar_A when window name starts with "A -", Toolbar_B when "B -", and Toolbar_C always. /// { /// ta.ShowToolbarWhenWindowName(Toolbar_A, "A -*"); /// ta.ShowToolbarWhenWindowName(Toolbar_B, "B -*"); /// if (ta.Later == 0) Toolbar_C(ta); /// }; /// ]]> /// public void ShowToolbarWhenWindowName(Func tbFunc, [ParamString(PSFormat.Wildex)] string windowName) { var wild = new wildex(windowName); ShowToolbarWhenWindowName(tbFunc, o => wild.Match(o.Name)); } //rejected: shorter syntax. Limited, eg can't use the same trigger for multiple toolbars. Examples: // Triggers.Window[TWEvent.ActiveOnce, "*Google Chrome", "Chrome_WidgetWin_1", whenName: "NuGet*"] = Toolbar_Chrome1; // Triggers.Window.Toolbar(Toolbar_Chrome1, "*Google Chrome", "Chrome_WidgetWin_1", whenName: "NuGet*"); /// /// Shows or hides a window-attached toolbar depending on window name. /// Use in window trigger action, like in examples. /// /// Toolbar function. Let it create a window-attached toolbar and returns the object. /// Callback function that returns true if the toolbar should be visible. /// /// Show toolbar Toolbar_Chrome2 on a Chrome window when web page URL starts with "https://www.youtube.com/". /// ta.ShowToolbarWhenWindowName(Toolbar_Chrome2, w => { /// var e = w.Elm["web:DOCUMENT"].Find(-1); /// return e != null && e.Value.Starts("https://www.youtube.com/"); /// }); /// ]]> /// public void ShowToolbarWhenWindowName(Func tbFunc, Func windowName) { var mi = tbFunc.Method; (t_mt ??= new()).TryGetValue(mi, out var v); if (!windowName(Window)) { v?.Close(); return; } if (v != null) return; v = tbFunc(this); if (v != null) { t_mt[mi] = v; v.Closed += () => { t_mt.Remove(mi); }; } else { t_mt.Remove(mi); } } [ThreadStatic] static Dictionary t_mt; } ================================================ FILE: Au/UI objects/CaptureScreen.cs ================================================ using System.Drawing; using System.Drawing.Imaging; namespace Au.More { /// /// Contains functions and tools to capture image, color, window, rectangle or point from screen. /// public static class CaptureScreen { #region capture image /// /// Creates image from a rectangle of screen pixels. /// /// Rectangle in screen. /// Empty rectangle. /// Failed. Probably there is not enough memory for bitmap of this size (width*height*4 bytes). /// /// /// public static Bitmap Image(RECT r) { using var c = new CaptureScreenImage(); c.Capture(r); return c.ToBitmap(); } /// /// Creates image from a rectangle of window client area pixels. /// /// Window or control. /// Rectangle in w client area coordinates. If null, uses w.ClientRect. /// Invalid w. /// The rectangle is empty or does not intersect with the window's client area. /// Failed. For example there is not enough memory for bitmap of this size (width*height*4 bytes). /// /// If flags contains WindowDC (default) or PrintWindow: /// - If the window is partially or completely transparent, captures its non-transparent view. /// - If the window is DPI-scaled, captures its non-scaled view. However r must contain scaled coordinates. /// public static Bitmap Image(wnd w, RECT? r = null, CIFlags flags = CIFlags.WindowDC) { using var c = new CaptureScreenImage(); if (!c.Capture(w, r, flags)) return null; return c.ToBitmap(); } /// /// Gets pixel colors from a rectangle in screen. /// /// 2-dimensional array [row, column] containing pixel colors in 0xAARRGGBB format. Alpha 0xFF. /// Rectangle in screen. /// Empty rectangle. /// Failed. Probably there is not enough memory for bitmap of this size (width*height*4 bytes). /// /// /// public static uint[,] Pixels(RECT r) { using var c = new CaptureScreenImage(); c.Capture(r); return c.ToArray2D(); } /// /// Gets pixel colors from a rectangle in window client area. /// /// 2-dimensional array [row, column] containing pixel colors in 0xAARRGGBB format. Alpha 0xFF. /// public static uint[,] Pixels(wnd w, RECT? r = null, CIFlags flags = CIFlags.WindowDC) { using var c = new CaptureScreenImage(); if (!c.Capture(w, r, flags)) return null; return c.ToArray2D(); } /// /// Gets color of a screen pixel. /// /// x y in screen. /// Pixel color in 0xAARRGGBB format. Alpha 0xFF. Returns 0 if fails, eg if x y is not in screen. public static unsafe uint Pixel(POINT p) { using var dc = new ScreenDC_(); uint R = Api.GetPixel(dc, p.x, p.y); if (R == 0xFFFFFFFF) return 0; return ColorInt.SwapRB(R) | 0xFF000000; } /// /// Gets color of a window pixel. /// /// x y in w client area. /// Pixel color in 0xAARRGGBB format. Alpha 0xFF. /// public static unsafe uint Pixel(wnd w, POINT p, CIFlags flags = CIFlags.WindowDC) { using var c = new CaptureScreenImage(); if (!c.Capture(w, new(p.x, p.y, 1, 1), flags)) return 0; return c.Pixels[0]; } #endregion #region capture image UI /// /// UI for capturing an image, color or rectangle on screen. /// /// false if canceled. /// Receives results. /// /// A window to minimize temporarily. /// A window to capture immediately instead of waiting for F3 key. Used only with a "get window pixels" flag. /// /// Gets all screen pixels and shows in a full-screen topmost window, where the user can select an area. /// /// Cannot capture windows that are always on top of normal topmost windows: 1. Start menu. 2. Topmost windows of UAC uiAccess processes (rare). /// public static bool ImageColorRectUI(out CIUResult result, CIUFlags flags = 0, AnyWnd owner = default, wnd wCapture = default) { result = default; switch (flags & (CIUFlags.Image | CIUFlags.Color | CIUFlags.Rectangle)) { case 0 or CIUFlags.Image or CIUFlags.Color or CIUFlags.Rectangle: break; default: throw new ArgumentException(); } List amw = new(); try { if (!owner.IsEmpty) { if (wCapture.Is0) { using (new inputBlocker(BIEvents.MouseClicks)) { var w = owner.Hwnd.Get.RootOwnerOrThis(); if (!w.Is0) { w.ShowMinimized(1); amw.Add(w); //also minimize editor etc if need for (int i = 0; i < 7; i++) { wait.doEvents(10); w = wnd.active; if (!w.IsOfThisProcess || w.IsMinimized) break; w = w.Get.RootOwnerOrThis(); w.ShowMinimized(1); amw.Add(w); } } wait.doEvents(300); //time for animations } } else { var w = owner.Hwnd; if (w.IsMinimized) amw.Add(w); } } bool windowPixels = flags.HasAny(CIUFlags.WindowDC | CIUFlags.PrintWindow); g1: RECT rs = screen.virtualScreen; //RECT rs = screen.primary.Rect; //for testing, to see print output in other screen Bitmap bs; wnd wTL = default; RECT rc = default; SIZE size = default; var avw = windowPixels ? null : wnd.getwnd.allWindows(onlyVisible: true) .Where(o => !(o.IsMinimized || o.IsCloaked)) .Select(o => (w: o, r: o.ClientRectInScreen)).ToArray(); if (windowPixels) { if (!wCapture.Is0) { wTL = wCapture; wCapture = default; } else { if (!_WaitForHotkey("Press F3 to select window from mouse pointer. Or Esc.")) return false; wTL = wnd.fromMouse(WXYFlags.NeedWindow); } rc = wTL.ClientRect; using var bw = Image(wTL, rc, flags.ToCIFlags_()); bs = new Bitmap(rs.Width, rs.Height); using var g = Graphics.FromImage(bs); g.Clear(Color.Gray); wTL.MapClientToScreen(ref rc); g.DrawImage(bw, rc.left - rs.left, rc.top - rs.top); size = bw.Size; } else { bs = Image(rs); } var wui = new _ImageUIWindow(); switch (wui.Show(bs, flags, rs)) { case 1: break; case 2: if (!windowPixels && !_WaitForHotkey("Press F3 when ready for new screenshot. Or Esc.")) return false; goto g1; default: return false; } var r = wui.Result; //if the window is DPI-scaled, scale r.rect if (windowPixels && (size.width != rc.Width || size.height != rc.Height) && Dpi.AwarenessContext.Available) { //Win10+, to match the unscaling code which can't support older OS int dpiw = Dpi.OfWindow(wTL), dpis = screen.of(wTL).Dpi; var rr = r.rect; r.rect = RECT.FromLTRB( rc.left + Math2.MulDiv(rr.left - rc.left, dpis, dpiw), rc.top + Math2.MulDiv(rr.top - rc.top, dpis, dpiw), rc.left + Math2.MulDiv(rr.right - rc.left, dpis, dpiw), rc.top + Math2.MulDiv(rr.bottom - rc.top, dpis, dpiw) ); r.dpiScale = (double)dpis / dpiw; } else r.dpiScale = 1; r.w = _WindowFromRect(r, wTL); //if that window is eg a menu, it probably disappeared. We get correct screenshot, but wrong window. // Workaround: try to detect it and set r.possiblyWrongWindow. // Then the 'find image' tool will prompt the user to capture the window with the 'find window' tool. if (windowPixels) r.possiblyWrongWindow = r.w.Window != wTL; else if (!r.w.Is0) { var w1 = avw.FirstOrDefault(o => o.r.Contains(r.rect)).w; if (w1 != r.w.Window) r.possiblyWrongWindow = !w1.IsVisible || w1.IsCloaked || w1.IsMinimized; } result = r; } finally { if (amw.Count > 0) { for (int i = amw.Count; --i >= 0;) try { amw[i].ShowNotMinimized(); } catch { } amw[0].ActivateL(); } Api.GetKeyState(1); //let OS update key states. Else the API later may not work eg in wndproc on some messages. } return true; static wnd _WindowFromRect(CIUResult r, wnd wTL) { //after closing our window, may need several ms until OS sets correct Z order. Until that may get different w1 and w2. Thread.Sleep(25); wnd w1, w2; var r1 = r.rect; if (!wTL.Is0 && wTL.MapScreenToClient(ref r1) && wTL.ClientRect.Contains(r1)) { w1 = wTL.ChildFromXY((r1.left, r1.top), WXYCFlags.OrThis); w2 = (r.image == null) ? w1 : wTL.ChildFromXY((r1.right - 1, r1.bottom - 1), WXYCFlags.OrThis); } else { w1 = wnd.fromXY((r.rect.left, r.rect.top)); w2 = (r.image == null) ? w1 : wnd.fromXY((r.rect.right - 1, r.rect.bottom - 1)); } if (w2 != w1 || !_IsInClientArea(w1)) { wnd w3 = w1.Window, w4 = w2.Window; w1 = (w4 == w3 && _IsInClientArea(w3)) ? w3 : default; } return w1; bool _IsInClientArea(wnd w) => w.GetClientRect(out var rc, true) && rc.Contains(r.rect); } static bool _WaitForHotkey(string info) { using (osdText.showText(info, Timeout.Infinite)) { //try { keys.waitForHotkey(0, KKey.F3); } //catch(AuException) { dialog.showError("Failed to register hotkey F3"); return false; } return KKey.F3 == keys.waitForKeys(0, k => !k.IsUp && k.Key is KKey.F3 or KKey.Escape, block: true); } } } class _ImageUIWindow { wnd _w; Bitmap _img; bool _paintedOnce; bool _magnMoved; bool _capturing; CIUFlags _flags; MouseCursor _cursor; SIZE _textSize; int _dpi; int _res; public CIUResult Result; /// 0 Cancel, 1 OK, 2 Retry. public int Show(Bitmap img, CIUFlags flags, RECT r) { _img = img; _flags = flags; //TODO3: cursor almost invisible on my 200% DPI tablet (somehow transparent). Test on true 200% DPI screen. _cursor = MouseCursor.Load(ResourceUtil.GetBytes("resources/red_cross_cursor.cur"), 32); _dpi = screen.primary.Dpi; _w = WndUtil.CreateWindow(_WndProc, true, WndUtil.WindowClassDWP_, "Au.CaptureScreen", WS.POPUP | WS.VISIBLE, WSE.TOOLWINDOW | WSE.TOPMOST, r.left, r.top, r.Width, r.Height); _w.ActivateL(); try { while (Api.GetMessage(out var m) && m.message != Api.WM_APP) { switch (m.message) { case Api.WM_KEYDOWN when !_capturing: switch ((KKey)(int)m.wParam) { case KKey.Escape: return 0; case KKey.F3: return 2; } break; case Api.WM_RBUTTONUP when m.hwnd == _w: switch (popupMenu.showSimple("1 Retry\tF3|2 Cancel\tEsc", owner: _w)) { case 1: return 2; case 2: return 0; } break; } Api.DispatchMessage(m); } } finally { var w = _w; _w = default; Api.DestroyWindow(w); } return _res; } nint _WndProc(wnd w, int msg, nint wParam, nint lParam) { //WndUtil.PrintMsg(w, msg, wParam, lParam); switch (msg) { case Api.WM_NCDESTROY: _img.Dispose(); _cursor?.Dispose(); if (_w != default) { _w = default; _w.Post(Api.WM_APP); } break; case Api.WM_SETCURSOR: Api.SetCursor(_cursor.Handle); return 1; case Api.WM_ERASEBKGND: return default; case Api.WM_PAINT: var dc = Api.BeginPaint(w, out var ps); _WmPaint(dc); Api.EndPaint(w, ps); return default; case Api.WM_MOUSEMOVE: _WmMousemove(Math2.NintToPOINT(lParam)); break; case Api.WM_LBUTTONDOWN: _WmLbuttondown(Math2.NintToPOINT(lParam)); break; } return Api.DefWindowProc(w, msg, wParam, lParam); } unsafe void _WmPaint(IntPtr dc) { #if true using var bd = _img.Data(ImageLockMode.ReadOnly); var bi = new Api.BITMAPINFO(bd.Width, -bd.Height); Api.SetDIBitsToDevice(dc, 0, 0, bd.Width, bd.Height, 0, 0, 0, bd.Height, (void*)bd.Scan0, &bi); #else //very slow using var g = Graphics.FromHdc(dc); g.DrawImageUnscaled(_img, 0, 0); #endif _paintedOnce = true; } void _WmMousemove(POINT pc) { if (!_paintedOnce) return; //format text to draw below magnifier string text; using (new StringBuilder_(out var s)) { var ic = _flags & (CIUFlags.Image | CIUFlags.Color | CIUFlags.Rectangle); if (ic == 0) ic = CIUFlags.Image | CIUFlags.Color; bool canColor = ic.Has(CIUFlags.Color); if (canColor) { var color = _img.GetPixel(pc.x, pc.y).ToArgb() & 0xffffff; s.Append("Color #").Append(color.ToString("X6")).Append('\n'); } if (ic == CIUFlags.Color) { s.Append("Click to capture color.\n"); } else if (ic == CIUFlags.Rectangle) { s.Append("Mouse-drag to capture rectangle.\n"); } else if (!canColor) { s.Append("Mouse-drag to capture image.\n"); } else { s.Append("Mouse-drag to capture image,\nor Ctrl+click to capture color.\n"); } s.Append("More: right-click"); //" cancel: key Esc\n retry: key F3 ... F3" text = s.ToString(); } var font = NativeFont_.RegularCached(_dpi); int magnWH = Dpi.Scale(200, _dpi) / 10 * 10; //width and height of the magnified image without borders etc if (_textSize == default) using (var tr = new FontDC_(font)) _textSize = tr.MeasureDT(text, TFFlags.NOPREFIX); int width = Math.Max(magnWH, _textSize.width) + 2, height = magnWH + 4 + _textSize.height; using var mb = new MemoryBitmap(width, height); var dc = mb.Hdc; using var wdc = new WindowDC_(_w); //draw frames and color background. Also erase magnifier, need when near screen edges. Api.FillRect(dc, (0, 0, width, height), Api.GetStockObject(4)); //BLACK_BRUSH //copy from captured screen image to magnifier image. Magnify 5 times. int k = magnWH / 10; Api.StretchBlt(dc, 1, 1, magnWH, magnWH, wdc, pc.x - k, pc.y - k, k * 2, k * 2, Api.SRCCOPY); //draw red crosshair k = magnWH / 2; using (var pen = new GdiPen_(0xff)) { pen.DrawLine(dc, (k, 1), (k, magnWH + 1)); pen.DrawLine(dc, (1, k), (magnWH + 1, k)); } //draw text below magnifier var rc = new RECT(1, magnWH + 2, _textSize.width, _textSize.height); Api.SetTextColor(dc, 0x32CD9A); //Color.YellowGreen Api.SetBkMode(dc, 1); var oldFont = Api.SelectObject(dc, font); Api.DrawText(dc, text, ref rc, TFFlags.NOPREFIX); Api.SelectObject(dc, oldFont); //set magninifier position far from cursor var pm = new POINT(4, 4); _w.MapScreenToClient(ref pm); int xMove = magnWH * 3; if (_magnMoved) pm.Offset(xMove, 0); var rm = new RECT(pm.x, pm.y, width, height); rm.Inflate(magnWH / 2, magnWH / 2); if (rm.Contains(pc)) { Api.InvalidateRect(_w, (pm.x, pm.y, width, height)); _magnMoved ^= true; pm.Offset(_magnMoved ? xMove : -xMove, 0); } Api.BitBlt(wdc, pm.x, pm.y, width, height, dc, 0, 0, Api.SRCCOPY); } void _WmLbuttondown(POINT p0) { if (Result != null) return; //bool isAnyShape = false; //rejected. Not useful. bool isColor = false; var ic = _flags & (CIUFlags.Image | CIUFlags.Color | CIUFlags.Rectangle); if (ic == CIUFlags.Color) { isColor = true; } else { var mod = keys.gui.getMod(); if (mod != 0 && ic == CIUFlags.Rectangle) return; switch (mod) { case 0: break; case KMod.Ctrl when ic == 0: isColor = true; break; default: return; } } Result = new CIUResult(); var r = new RECT(p0.x, p0.y, 0, 0); if (isColor) { Result.color = (uint)_img.GetPixel(p0.x, p0.y).ToArgb(); r.right++; r.bottom++; } else { var pen = Pens.Red; bool notFirstMove = false; _capturing = true; try { if (!WndUtil.DragLoop(_w, MButtons.Left, m => { if (m.msg.message != Api.WM_MOUSEMOVE) return; POINT p = m.msg.pt; _w.MapScreenToClient(ref p); using var g = Graphics.FromHwnd(_w.Handle); if (notFirstMove) { //erase prev rect r.right++; r.bottom++; g.DrawImage(_img, r, r, GraphicsUnit.Pixel); //FUTURE: prevent flickering. Also don't draw under magnifier. } else notFirstMove = true; r = RECT.FromLTRB(p0.x, p0.y, p.x, p.y); r.Normalize(true); g.DrawRectangle(pen, r); })) { //Esc key etc Api.InvalidateRect(_w); return; } } finally { _capturing = false; } r.right++; r.bottom++; if (r.NoArea) { Api.DestroyWindow(_w); return; } if (ic != CIUFlags.Rectangle) { Result.image = _img.Clone(r, PixelFormat.Format32bppArgb); } } _w.MapClientToScreen(ref r); Result.rect = r; if (isColor) { //bad things may happen if this window closed while the mouse button or Ctrl pressed for (int i = 200; --i >= 0 && (Api.GetKeyState(1) < 0 || Api.GetKeyState(17) < 0);) wait.doEvents(15); } _res = 1; Api.DestroyWindow(_w); } } #endregion #region other /// /// UI for capturing a rectangle, point or/and window on screen with Shift key. /// /// true if captured, false if pressed Esc. /// /// /// Get rectangle in window client area. /// public static unsafe bool RectPointWindowUI(out CRUResult result, CRUType type, bool rectInClient = false, WXYFlags wxyFlags = 0) { result = default; var wxyFlags2 = wxyFlags & (WXYFlags.NeedWindow | WXYFlags.NeedControl); var s = type switch { CRUType.Window => "Press Shift to capture %", CRUType.Rect => "Shift+mouse move to capture rectangle on screen.", CRUType.Point => "Press Shift to capture mouse coordinates.", CRUType.WindowAndPoint => "Press Shift to capture mouse coordinates in a %.", CRUType.WindowAndRect => "Shift+mouse move to capture rectangle in a %.", _ => "Press Shift to capture %.\nOr Shift+mouse move to capture rectangle in a %." }; s = s.Replace("%", wxyFlags2 switch { WXYFlags.NeedWindow => "window", WXYFlags.NeedControl => "control", _ => "window or control" }); s += "\nOr press Esc to cancel."; using var osd = osdText.showText(s, -1); wnd w = default, wClip = default; bool needWindow = type is not (CRUType.Rect or CRUType.Point); if (needWindow) { //draw black rectangle around window or control from mouse using var osrw = new osdRect { }; osrw.Show(); for (; ; ) { wait.doEvents(12); if (keys.isPressed(KKey.Escape)) return false; w = wnd.fromMouse(wxyFlags); osrw.Rect = w.Rect; if (keys.isShift) break; } if (w == default) return false; } else { for (; ; ) { wait.doEvents(12); if (keys.isPressed(KKey.Escape)) return false; if (keys.isShift) break; } } var p = mouse.xy; RECT r = new(p.x, p.y, 0, 0); if (type is not (CRUType.Window or CRUType.Point or CRUType.WindowAndPoint)) { //draw red rectangle if (needWindow) { wClip = wxyFlags2 == WXYFlags.NeedControl ? w : w.Window; var rw = rectInClient ? wClip.ClientRectInScreen : wClip.Rect; Api.ClipCursor(&rw); } try { using var osrr = new osdRect { Color = 0xff0000, Thickness = 1, Rect = r }; osrr.Show(); for (var r1 = r; ;) { wait.doEvents(12); if (keys.isPressed(KKey.Escape)) return false; if (!keys.isShift) break; p = mouse.xy; r1.right = p.x; r1.bottom = p.y; r = r1; r.Normalize(true); osrr.Rect = r; } } finally { if (needWindow) Api.ClipCursor(null); } if (needWindow && w != wClip) { //if rect spans multiple controls, get top-level window var w2 = wnd.fromMouse(wxyFlags); if (w2 != w) w = wClip; } } if (needWindow && rectInClient) w.MapScreenToClient(ref r); result = new(w, !r.NoArea, r); return true; } #endregion } /// /// Captures image pixels from screen or window. /// /// /// This class is used by , and . Also you can use it directly. For example it can get pixels directly without copying to a or array. /// /// How to use: /// 1. Create variable. /// 2. Call . /// 3. Call other functions to get result in various formats. /// 4. If need, repeat 2-3 (for example when waiting for image). /// 5. Dispose (important). /// /// Pixel format: Format32bppArgb, alpha 0xff. /// /// /// /// public unsafe sealed class CaptureScreenImage : IDisposable { MemoryBitmap _mb; uint* _pixels; int _width, _height; int _dibWidth, _dibHeight; bool _alphaOk; //_DwmThumbnail _dwm; /// /// Frees image memory. /// public void Dispose() { _mb?.Dispose(); _mb = null; _pixels = null; _width = _height = 0; //_dwm?.Dispose(); _dwm = null; } internal void SetExternalData_(uint* pixels, int width, int height) { Debug.Assert(_mb == null); _pixels = pixels; _width = width; _height = height; } /// /// Let Capture don't set alpha = 0xff. Slightly faster. Then pixelformat will be Rgb instead of Argb. /// Default false. /// internal bool DontSetAlpha_ { get; set; } /// /// Captures image from window client area into memory stored in this variable. /// /// Window or control. /// Rectangle in w client area coordinates. If null, uses w.ClientRect. /// false if r empty or not in the client area and used flag Relaxed (else exception). public bool Capture(wnd w, RECT? r = null, CIFlags flags = CIFlags.WindowDC) => _Capture(w.ThrowIf0(), r, flags); /// /// Captures image from screen into memory stored in this variable. /// /// If r empty, return false instead of exception. /// false if r empty and relaxed true (else exception). public bool Capture(RECT r, bool relaxed = false) => _Capture(default, r, flags: relaxed ? CIFlags.Relaxed : 0); bool _Capture(wnd w, RECT? rect, CIFlags flags) { const CIFlags c_howMask = CIFlags.WindowDC | CIFlags.PrintWindow /*| CIFlags.WindowDwm*/; if ((flags & c_howMask) is not (0 or CIFlags.WindowDC or CIFlags.PrintWindow /*or CIFlags.WindowDwm*/)) throw new ArgumentException(); bool fromWindow = flags.HasAny(c_howMask), printWindow = flags.Has(CIFlags.PrintWindow); RECT r = rect ?? default, rc = default; if (!w.Is0) { //if (flags.Has(CIFlags.WindowDwm)) { //w must be top-level window // var ww = w.Window; // if (ww != w) { // ww.ThrowIf0(); // if (rect == null) r = w.ClientRect; // w.MapClientToClientOf(ww, ref r); // rect = r; // w = ww; // } //} var rc2 = w.ClientRect; bool dpiScaled = fromWindow && Dpi.IsWindowVirtualizedWin10_(w); using var dac = dpiScaled ? new Dpi.AwarenessContext(w) : default; if (!w.GetClientRect(out rc)) w.ThrowUseNative(); if (rect == null || r == rc2) { r = rc; } else { if (dpiScaled) { //unscale r dac.Dispose(); var ww = w.Window; int dpiw = Dpi.OfWindow(ww), dpis = screen.of(ww).Dpi; r = RECT.FromLTRB(Math2.MulDiv(r.left, dpiw, dpis), Math2.MulDiv(r.top, dpiw, dpis), Math2.MulDiv(r.right, dpiw, dpis), Math2.MulDiv(r.bottom, dpiw, dpis)); } if (!r.Intersect(rc)) return flags.Has(CIFlags.Relaxed) ? false : throw new ArgumentException("rectangle not in window"); } //if (flags.Has(CIFlags.WindowDwm)) { // _dwm ??= new(); // if (!_dwm.Init(w, r, dpiScaled)) return flags.Has(CIFlags.Relaxed) ? false : throw new ArgumentException("rectangle not in window"); // w = _dwm.WndThumbnail; //} } if (r.NoArea) return flags.Has(CIFlags.Relaxed) ? false : throw new ArgumentException("empty rectangle"); int dibWidth = printWindow ? rc.Width : r.Width, dibHeight = printWindow ? rc.Height : r.Height; if (_mb == null || dibWidth != _dibWidth || dibHeight != _dibHeight) { var bi = new Api.BITMAPINFO(dibWidth, -dibHeight); var dib = Api.CreateDIBSection(default, bi, 0, out var pixels); if (dib == default) throw new AuException("*create memory bitmap of specified size"); _pixels = pixels; _mb ??= new MemoryBitmap(); _mb.Attach(dib); //and deletes old bitmap _dibWidth = dibWidth; _dibHeight = dibHeight; } _width = r.Width; _height = r.Height; if (printWindow) { //must capture entire client area var pw = Api.PW_CLIENTONLY; if (osVersion.minWin8_1) pw |= Api.PW_RENDERFULLCONTENT; //PW_RENDERFULLCONTENT is new in Win8.1. Undocumented in MSDN, but defined in h. Then works with windows like Chrome, winstore. // Bug: from some controls randomly gets partially painted image. Eg classic toolbar, treeview. // Rejected: if PrintClient|WindowDC, capture without PW_RENDERFULLCONTENT. Makes no sense. if (!Api.PrintWindow(w, _mb.Hdc, pw)) w.ThrowNoNative("*get pixels"); if (r.left != 0 || r.top != 0 || _width != dibWidth) { //move pixels to the start of the bitmap memory for (int y = r.top; y < r.bottom; y++) { var spanFrom = new Span(_pixels + y * dibWidth + r.left, _width); var spanTo = new Span(_pixels + (y - r.top) * _width, _width); spanFrom.CopyTo(spanTo); } } //} else if (flags.Has(CIFlags.WindowDwm)) { // if (!Api.PrintWindow(w, _mb.Hdc, Api.PW_CLIENTONLY | Api.PW_RENDERFULLCONTENT)) w.ThrowNoNative("*get pixels"); // _alphaOk = true; // return true; } else { if (!w.Is0 && !fromWindow) { w.MapClientToScreen(ref r); w = default; } using var dc = new WindowDC_(w); if (dc.Is0) w.ThrowNoNative("*get pixels"); uint rop = !w.Is0 ? Api.SRCCOPY : Api.SRCCOPY | Api.CAPTUREBLT; Api.BitBlt(_mb.Hdc, 0, 0, _width, _height, dc, r.left, r.top, rop); //fails only if a HDC is invalid } if (_alphaOk = !DontSetAlpha_) { byte* p = (byte*)_pixels, pe = p + _width * _height * 4; for (p += 3; p < pe; p += 4) *p = 0xff; } return true; } /// /// Width of the captured image. /// public int Width => _width; /// /// Height of the captured image. /// public int Height => _height; /// /// Pixels of the captured image. /// public uint* Pixels => _pixels; /// /// Copies pixels of the captured image to new 1D array. /// [SkipLocalsInit] public uint[] ToArray1D() { var a = GC.AllocateUninitializedArray(_height * _width); fixed (uint* p = a) { MemoryUtil.Copy(_pixels, p, _width * _height * 4); } return a; } /// /// Copies pixels of the captured image to new 2D array [row, column]. /// public uint[,] ToArray2D() { var a = new uint[_height, _width]; fixed (uint* p = a) { MemoryUtil.Copy(_pixels, p, _width * _height * 4); } return a; } /// /// Creates new Bitmap from pixels of the captured image. /// public Bitmap ToBitmap() { var b = new Bitmap(_width, _height, _alphaOk ? PixelFormat.Format32bppArgb : PixelFormat.Format32bppRgb); GC_.AddObjectMemoryPressure(b, _width * _height * 4); using var d = b.Data(new(0, 0, _width, _height), ImageLockMode.ReadWrite); MemoryUtil.Copy(_pixels, (uint*)d.Scan0, _width * _height * 4); return b; } //rejected. Unreliable. Not all windows that require PW_RENDERFULLCONTENT have WS_EX_NOREDIRECTIONBITMAP. Eg Chrome on Win8.1. //static uint _GetPrintWindowFlags(CIFlags flags, wnd w, RECT r) { // if (!flags.Has(CIFlags.PrintWindow)) return 0; // var f = Api.PW_CLIENTONLY; // if (osVersion.minWin8_1) { // var wtl = w.Window; // if (wtl.HasExStyle(WSE.NOREDIRECTIONBITMAP)) f |= Api.PW_RENDERFULLCONTENT; // else // api.EnumChildWindows(wtl, (c, _) => { // if (c.HasExStyle(WSE.NOREDIRECTIONBITMAP) && (c == w || (c.IsVisible && c.GetRectIn(w, out var rr) && rr.IntersectsWith(r)))) { // f |= Api.PW_RENDERFULLCONTENT; // return false; // } // return true; // }, 0); // } // if (flags.Has(CIFlags.WindowDC) && 0 == (f & Api.PW_RENDERFULLCONTENT)) f = 0; // return f; //} } } namespace Au.Types { /// /// Used with functions. /// [Flags] public enum CIFlags { /// WindowDC = 1, /// PrintWindow = 2, ///// //WindowDwm = 4, //note: the above values must be the same in CIFlags, CIUFlags, IFFlags, OcrFlags. /// /// Flag: don't throw exception when the specified rectangle or point does not intersect with the window client area or when the rectangle is empty. Instead return null or 0. /// Relaxed = 0x100, //rejected. Or would need a tool to capture rect/pont in logical coord. ///// ///// Flag: the specified rectangle or point uses logical (non-scaled) coordinates when the window is DPI-scaled. Used only with flags WindowDC (default) or PrintWindow. ///// //RectLogical = 0x200, } static partial class ExtMisc { internal static CIFlags ToCIFlags_(this CIUFlags t) => (CIFlags)t & (CIFlags.WindowDC | CIFlags.PrintWindow /*| CIFlags.WindowDwm*/); internal static CIFlags ToCIFlags_(this IFFlags t) => (CIFlags)t & (CIFlags.WindowDC | CIFlags.PrintWindow /*| CIFlags.WindowDwm*/); internal static CIFlags ToCIFlags_(this OcrFlags t) => (CIFlags)t & (CIFlags.WindowDC | CIFlags.PrintWindow /*| CIFlags.WindowDwm*/); } /// /// Flags for . /// /// /// Only one of flags Image, Color and Rectangle can be used. If none, can capture image or color. /// [Flags] public enum CIUFlags { /// WindowDC = 1, /// PrintWindow = 2, //could not make it work well with "glass" areas. ///// //WindowDwm = 4, //note: the above values must be the same in CIFlags, CIUFlags, IFFlags, OcrFlags. /// Can capture only image, not color. Image = 0x100, /// Can capture only color, not image. Color = 0x200, /// Capture only rectangle, not image/color. Rectangle = 0x400, } #pragma warning disable 1591 //XML doc [Flags, Obsolete("Renamed to CIUFlags"), EditorBrowsable(EditorBrowsableState.Never)] public enum ICFlags { WindowDC = 1, PrintWindow = 2, Image = 0x100, Color = 0x200, Rectangle = 0x400, } [Obsolete("Renamed to CIUResult"), EditorBrowsable(EditorBrowsableState.Never)] public class ICResult : CIUResult { } #pragma warning restore 1591 //XML doc /// /// Results of . /// public class CIUResult { /// /// Captured image. /// null if captured single pixel color or used flag . /// public Bitmap image; /// /// Captured color in 0xAARRGGBB format. Alpha 0xFF. /// public uint color; /// /// Location of the captured image or rectangle, in screen coordinates. /// public RECT rect; /// /// Window or control containing the captured image or rectangle, if whole image is in its client area. /// In some cases may be incorrect, for example if windows moved/opened/closed/etc while capturing. /// public wnd w; /// /// If used flags to get window pixels and the window is DPI-scaled (smaller when capturing), on Windows 10 and later contains the scale factor. Else 1. /// public double dpiScale; /// /// If true, most likely is incorrect window, because the window that was there before capturing disappeared while capturing, for example it was a popup menu. /// If captured from screen (without flags like WindowDC), may be correct even if this is true (can't detect reliably), else certainly incorrect. /// public bool possiblyWrongWindow; } /// /// UI type. /// public enum CRUType { /// Capture only window. Window, /// Capture only rectangle in screen. Rect, /// Capture rectangle in window. WindowAndRect, /// Capture window and optionally rectangle in it. WindowOrRect, /// Capture only point (coordinates) in screen. Point, /// Capture point in window. WindowAndPoint, } /// /// results. /// public record struct CRUResult(wnd w, bool hasRect, RECT r); } ================================================ FILE: Au/UI objects/OcrGoogleCloud.cs ================================================ using System.Drawing; using System.Text.Json.Nodes; using System.Net.Http; namespace Au.More; /// /// This OCR engine uses Google Cloud Vision API. /// /// /// Sends image to Google Cloud and gets results. The OCR engine is accurate but much slower than the default engine or Tesseract. Depends on internet connection speed. /// /// To use this engine, need to have a Google Cloud account, enable Vision API and get API key. The service isn't free, but 1000 or so requests/month are free. /// public class OcrGoogleCloud : IOcrEngine { string _apiKey; /// API key. public OcrGoogleCloud(string apiKey) { _apiKey = apiKey; } /// /// Feature type, like "TEXT_DETECTION". Or JSON of features array content, like """{ "type": "TEXT_DETECTION", "model": "builtin/latest" }""". /// If null, uses "DOCUMENT_TEXT_DETECTION". /// public string Features { get; set; } /// /// JSON of imageContext, like """{ "languageHints": [ "ja" ] }""". Optional. /// public string ImageContext { get; set; } /// public bool DpiScale { get; set; } /// /// Failed. public OcrWord[] Recognize(Bitmap b, bool dispose, double scale) { var b0 = b; b = IOcrEngine.PrepareBitmap(b, dispose, scale); var png = IOcrEngine.GetBitmapPngFileData(b); if (dispose || b != b0) b.Dispose(); var url = "https://vision.googleapis.com/v1/images:annotate?key=" + _apiKey; var feat = Features ?? "DOCUMENT_TEXT_DETECTION"; if (!feat.Starts('{')) feat = $$"""{ "type": "{{feat}}" }"""; string ic = ImageContext, ic2 = ic.NE() ? null : ",\r\n \"imageContext\": "; var requestJson = $$""" { "requests": [ { "features": [ {{feat}} ], "image": { "content": "{{Convert.ToBase64String(png)}}" }{{ic2}}{{ic}} } ] } """; //perf.first(); if (!internet.http_.TryPost(out var r, url, internet.jsonContent(requestJson), ["Accept-Encoding: br, gzip, deflate"], dontWait: true)) throw new AuException(r.Text(true)); //perf.next(); #if true //can be faster > 10 times. Also we use compression. Together it makes this part 100 times faster than the TryPost. var j = _ReadResponse(r); //perf.nw(); return _ParseJson(j["responses"][0], scale); #else var j=r.Json(); //perf.nw(); return _ParseJson(j["responses"][0], scale); #endif } //Reads response until "fullTextAnnotation". //Can make the download size smaller > 10 times. static unsafe JsonNode _ReadResponse(HttpResponseMessage rm) { var find = "\"fullTextAnnotation\":"u8; using var ab = new ArrayBuilder_() { Capacity = 250_000 }; int have = 0; using (var stream = rm.Content.ReadAsStream()) { for (; ; ) { if (have + 17000 > ab.Capacity) ab.ReAlloc(ab.Capacity * 2); int n = stream.Read(new Span(ab.Ptr + have, ab.Capacity - have - 10)); if (n == 0) break; int old = Math.Min(find.Length, have); int i = new RByte(ab.Ptr + have - old, n + old).IndexOf(find); if (i > 0) { i += have - old; while (ab.Ptr[i - 1] is 32 or 9 or 10 or 13 or (byte)',') i--; ab.Ptr[i++] = (byte)'}'; ab.Ptr[i++] = (byte)']'; ab.Ptr[i++] = (byte)'}'; have = i; break; } have += n; } } rm.Dispose(); return JsonNode.Parse(new RByte(ab.Ptr, have)); } static OcrWord[] _ParseJson(JsonNode j, double scale) { if (j["textAnnotations"]?.AsArray() is not { } ta) return []; //no text detected List a = new(); string text = null; int i = 0; foreach (var word in ta) { var s = (string)word["description"]; if (text == null) { text = s; } else { int i2 = text.Find(s, i); var sep = text[i..i2]; if (sep == "\n") sep = "\r\n"; a.Add(new(sep, s, _PolyToRect(word), scale)); i = i2 + s.Length; } } return a.ToArray(); static RECT _PolyToRect(JsonNode n) { var a = n["boundingPoly"]["vertices"].AsArray(); JsonNode tl = a[0], tr = a[1], br = a[2], bl = a[3]; return IOcrEngine.PolyToRect(tl["x"], tl["y"], tr["x"], tr["y"], br["x"], br["y"], bl["x"], bl["y"]); } } } ================================================ FILE: Au/UI objects/OcrMicrosoftAzure.cs ================================================ using System.Drawing; using System.Text.Json.Nodes; using System.Net.Http; namespace Au.More; /// /// This OCR engine uses Microsoft Azure Computer Vision OCR. /// /// /// Sends image to Microsoft Azure and gets results. The OCR engine is accurate but much slower than the default engine or Tesseract, and usually slower than Google Cloud. /// /// To use this engine, need to have a Microsoft Azure account and get API key and endpoint URL. The service isn't free, but 500 or so requests/month are free. /// public class OcrMicrosoftAzure : IOcrEngine { string _endpointUrl, _apiKey; /// Endpoint URL, like "https://xxxx.cognitiveservices.azure.com/". /// API key. public OcrMicrosoftAzure(string endpointUrl, string apiKey) { if (!endpointUrl.Like("https://*.cognitiveservices.azure.com/")) print.warning("endpointUrl should be like https://xxxx.cognitiveservices.azure.com/"); _endpointUrl = endpointUrl; _apiKey = apiKey; } /// public bool DpiScale { get; set; } /// /// Failed. public OcrWord[] Recognize(Bitmap b, bool dispose, double scale) { var b0 = b; b = IOcrEngine.PrepareBitmap(b, dispose, scale, 50, 50); var png = IOcrEngine.GetBitmapPngFileData(b); if (dispose || b != b0) b.Dispose(); var url = $"{_endpointUrl}formrecognizer/documentModels/prebuilt-read:analyze?api-version=2022-08-31"; //&stringIndexType=textElements var headers = new[] { "Ocp-Apim-Subscription-Key: " + _apiKey }; var requestJson = $$""" { "base64Source": "{{Convert.ToBase64String(png)}}" } """; if (!internet.http_.TryPost(out var r, url, internet.jsonContent(requestJson), headers)) throw new AuException(r.Text(true)); url = r.Headers.GetValues("Operation-Location").First(); //perf.next(); 500.ms(); var j = wait.until(new(90) { Period = 300 }, () => { var v = internet.http_.Get(url, headers: headers).Json(); return (string)v["status"] == "succeeded" ? v : null; }); //perf.nw(); return _ParseJson(j["analyzeResult"], scale); } static OcrWord[] _ParseJson(JsonNode j, double scale) { List a = new(); HashSet hs = new(); int i = 0; foreach (var page in j["pages"].AsArray()) { foreach (var line in page["lines"].AsArray()) { hs.Add((int)line["spans"][0]["offset"]); } foreach (var word in page["words"].AsArray()) { var sep = i++ == 0 ? null : hs.Contains((int)word["span"]["offset"]) ? "\r\n" : " "; a.Add(new(sep, (string)word["content"], _PolyToRect(word), scale)); } } return a.ToArray(); static RECT _PolyToRect(JsonNode n) { var a = n["polygon"].AsArray(); return IOcrEngine.PolyToRect(a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7]); } } } ================================================ FILE: Au/UI objects/OcrTesseract.cs ================================================ using Microsoft.Win32; using System.Drawing; using System.Drawing.Imaging; namespace Au.More; /// /// This OCR engine uses tesseract.exe from Tesseract installed on this computer. /// /// /// Slower than (the default engine). The accuracy is poor. /// Supports more languages. You choose what languages to install when you install Tesseract. /// /// Download Tesseract /// public class OcrTesseract : IOcrEngine { /// Full path of Tesseract folder. If null, uses path written in the registry by the installer. /// public OcrTesseract(string tesseractPath = null) { _tesseractPath = tesseractPath ?? Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Tesseract-OCR", "Path", null) as string; var exe = _tesseractPath + @"\tesseract.exe"; if (!filesystem.exists(exe).File) throw new FileNotFoundException("tesseract.exe not found. Download/install Tesseract, or set correct tesseractPath argument."); _tesseractExe = exe; } readonly string _tesseractPath, _tesseractExe; /// /// One or more of installed languages, like "deu" or "eng+deu". If null (default), uses "eng". /// public string Language { get; set; } /// /// Gets OCR languages that are installed on this computer and can be used for . /// public string[] AvailableLanguages { get { var a = new List(); run.console(s => { if (!s.NE() && !s.Ends(':') && !s.Eqi("osd")) a.Add(s); }, _tesseractExe, "--list-langs"); return a.ToArray(); //note: can be in subfolders, like "test/deu". } } ///// ///// Adds command line --psm (page segmentation mode). ///// Valid values are 0-13, but documented only 3 (default) and 6 (no segmentation). ///// //public int Psm { get; set; } = 3; /// /// Additional command line arguments. /// public string CommandLine { get; set; } /// public bool DpiScale { get; set; } = true; /// public OcrWord[] Recognize(Bitmap b, bool dispose, double scale) { var b0 = b; b = IOcrEngine.PrepareBitmap(b, dispose, scale); using var temp = new TempFile(".png"); b.Save(temp, ImageFormat.Png); if (dispose || b != b0) b.Dispose(); var cl = $"\"{temp}\" stdout"; if (!Language.NE()) cl += $" -l {Language}"; //if (Psm != 3) cl += $" --psm {Psm}"; if (!CommandLine.NE()) cl += $" {CommandLine}"; cl += " quiet tsv"; //must be at the end, else all -x parameters ignored if (0 != run.console(out string result, _tesseractExe, cl, encoding: Encoding.UTF8)) throw new AuException(result); //print.it(result); regexp rx = new(@"^(?:\d+\t){6}(\d+)\t(\d+)\t(\d+)\t(\d+)\t\S+\t(\S.*)$"); List a = new(); string sep = null; RECT pr = default; foreach (var s in result.Lines()) { switch (s[0]) { case '4': //line if (sep != null) sep = "\r\n"; break; case '5': //word if (rx.Match(s, out var m)) { //else text is space RECT r = new(m[1].Value.ToInt(), m[2].Value.ToInt(), m[3].Value.ToInt(), m[4].Value.ToInt()); //break line if big space between words if (sep == " " && r.left - pr.right > pr.Height + r.Height) sep = "\r\n"; pr = r; a.Add(new(sep, m[5].Value, r, scale)); sep = " "; } break; } } return a.ToArray(); } } ================================================ FILE: Au/UI objects/OcrWin10.cs ================================================ using System.Drawing; using System.Drawing.Imaging; namespace Au.More { /// /// The default OCR engine. Available on Windows 10 and later. /// /// /// Uses the Windows 10/11 OCR engine. It's the fastest. The accuracy is poor. /// If need better accuracy, use a cloud OCR engine (, ). /// If need a non-cloud OCR for older Windows, install Tesseract and use . Also it supports more languages. /// public unsafe class OcrWin10 : IOcrEngine { /// OS version older than Windows 10. public OcrWin10() { if (!osVersion.minWin10) throw new NotSupportedException("This OCR engine is available only on Windows 10 and later. Use another engine."); } /// /// Language, like "en-US". See . If null (default), uses the default OCR language of this computer. /// /// /// You can install languages in Windows Settings > Time and language > Language and region. Not all languages are supported. /// public string Language { get; set; } /// /// Gets OCR languages that are installed on this computer and can be used for . /// public (string tag, string displayName)[] AvailableLanguages { get { using var oes = WinRT.IOcrEngineStatics.CreateStatics(); using var a1 = oes.AvailableRecognizerLanguages; var a2 = new (string, string)[a1.Size]; for (int i = 0; i < a2.Length; i++) { using var lang = a1[i]; a2[i] = (lang.LanguageTag, lang.DisplayName); } return a2; } } /// public bool DpiScale { get; set; } = true; /// public OcrWord[] Recognize(Bitmap b, bool dispose, double scale) { var b0 = b; b = IOcrEngine.PrepareBitmap(b, dispose, scale, 50, 50); using var sb = WinRT.ISoftwareBitmap.FromBitmap(b); if (dispose || b != b0) b.Dispose(); using var engine = WinRT.IOcrEngine.CreateEngine(Language); using var result = engine.RecognizeAsync(sb).Await(); var a = new List(); string sep = null; using var lines = result.Lines; foreach (var line in lines.Items()) { using var words = line.Words; foreach (var word in words.Items()) { a.Add(new(sep, word.Text, word.BoundingRect, scale)); sep = " "; } sep = "\r\n"; } return a.ToArray(); } } } namespace Au.Types { #pragma warning disable 649, 169 //field never assigned/used static unsafe partial class WinRT { internal struct IOcrResult : IComPtr { IUnknown _u; public IUnknown U => _u; public void Dispose() => _u.Dispose(); public IVectorView Lines => _u.GetPtr>(6); //public string Text => _u.GetString(8); } internal struct IOcrLine : IComPtr { IUnknown _u; public IUnknown U => _u; public void Dispose() => _u.Dispose(); public IVectorView Words => _u.GetPtr>(6); //public string Text => _u.GetString(7); } internal struct IOcrWord : IComPtr { IUnknown _u; public IUnknown U => _u; public void Dispose() => _u.Dispose(); public RECT BoundingRect { get { HR(((delegate* unmanaged[Stdcall])_u[6])(_u, out var r)); return RECT.From(r, true); } } public string Text => _u.GetString(7); } [Guid("5BFFA85A-3384-3540-9940-699120D428A8")] internal struct IOcrEngineStatics : IComPtr { IUnknown _u; public IUnknown U => _u; public void Dispose() => _u.Dispose(); //public int MaxImageDimension { get; } public IVectorView AvailableRecognizerLanguages => _u.GetPtr>(7); //public bool IsLanguageSupported(ILanguage language); public IOcrEngine TryCreateFromLanguage(ILanguage language) { HR(((delegate* unmanaged[Stdcall])_u[9])(_u, language, out var r)); return r; } public IOcrEngine TryCreateFromUserProfileLanguages() => _u.GetPtr(10); public static IOcrEngineStatics CreateStatics() => Create("Windows.Media.Ocr.OcrEngine"); } internal struct IOcrEngine : IComPtr { IUnknown _u; public IUnknown U => _u; public void Dispose() => _u.Dispose(); public IAsyncOperation RecognizeAsync(ISoftwareBitmap bitmap) { HR(((delegate* unmanaged[Stdcall])_u[6])(_u, bitmap, out var r)); return r; } //public ILanguage RecognizerLanguage { get; } public static IOcrEngine CreateEngine(string language = null) { using var oes = IOcrEngineStatics.CreateStatics(); if (language.NE()) return oes.TryCreateFromUserProfileLanguages(); using var lf = Create("Windows.Globalization.Language"); using var lang = lf.CreateLanguage(language); return oes.TryCreateFromLanguage(lang); } } [Guid("DF0385DB-672F-4A9D-806E-C2442F343E86")] struct ISoftwareBitmapStatics : IComPtr { IUnknown _u; public IUnknown U => _u; public void Dispose() => _u.Dispose(); public ISoftwareBitmap CreateCopyFromBuffer(IBuffer source, int width, int height) { HR(((delegate* unmanaged[Stdcall])_u[9])(_u, source, 87, width, height, out var r)); //Bgra8 = 87 return r; } } internal struct ISoftwareBitmap : IComPtr { IUnknown _u; public IUnknown U => _u; public void Dispose() => _u.Dispose(); public static ISoftwareBitmap FromBitmap(Bitmap b) { using var d = b.Data(ImageLockMode.ReadOnly, b.PixelFormat == PixelFormat.Format32bppRgb ? PixelFormat.Format32bppRgb : PixelFormat.Format32bppArgb); if (d.Stride < 0) throw new ArgumentException(); using var cbs = Create("Windows.Security.Cryptography.CryptographicBuffer"); using var sbs = Create("Windows.Graphics.Imaging.SoftwareBitmap"); using var buffer = cbs.CreateFromByteArray(d.Height * d.Stride, (byte*)d.Scan0); return sbs.CreateCopyFromBuffer(buffer, d.Width, d.Height); } } [Guid("320B7E22-3CB0-4CDF-8663-1D28910065EB")] struct ICryptographicBufferStatics : IComPtr { IUnknown _u; public IUnknown U => _u; public void Dispose() => _u.Dispose(); //the easiest way to create IBuffer which is required to create ISoftwareBitmap public IBuffer CreateFromByteArray(int size, byte* value) { HR(((delegate* unmanaged[Stdcall])_u[9])(_u, size, value, out var r)); return r; } } struct IBuffer : IComPtr { IUnknown _u; public IUnknown U => _u; public void Dispose() => _u.Dispose(); } internal struct ILanguage : IComPtr { IUnknown _u; public IUnknown U => _u; public void Dispose() => _u.Dispose(); public string LanguageTag => _u.GetString(6); public string DisplayName => _u.GetString(7); //public string NativeName => _u.GetString(8); //public string Script => _u.GetString(9); } [Guid("9B0252AC-0C27-44F8-B792-9793FB66C63E")] internal struct ILanguageFactory : IComPtr { IUnknown _u; public IUnknown U => _u; public void Dispose() => _u.Dispose(); public ILanguage CreateLanguage(string languageTag) { using var s1 = new _Hstring(languageTag); HR(((delegate* unmanaged[Stdcall])_u[6])(_u, s1, out var r)); return r; } } } } ================================================ FILE: Au/UI objects/elm.cs ================================================ namespace Au; /// /// Represents a UI element. Clicks, gets properties, etc. /// /// /// /// UI elements are user interface (UI) parts that are accessible through programming interfaces (API). For example buttons, links, list items. /// This class can find them, get properties, click, etc. /// Web pages and most other windows support UI elements. /// /// /// An elm variable contains a COM interface pointer (IAccessible or other) and uses methods of that interface or/and related API. /// /// /// elm functions that get properties don't throw exception when the COM etc method failed (returned an error code of HRESULT type). /// Then they return "" (string properties), 0, false, null or empty collection, depending on return type. /// Applications implement UI elements differently, often with bugs, and their COM interface functions return a variety of error codes. /// It's impossible to reliably detect whether the error code means an error or the property is merely unavailable. /// These elm functions also set the last error code of this thread = the return value (HRESULT) of the COM function, and callers can use to get it. /// If returns 1 (S_FALSE), in most cases it's not an error, just the property is unavailable. On error it will probably be a negative error code. /// /// /// You can dispose elm variables to release the COM object, but it is not necessary (GC will do it later). /// /// /// An elm variable cannot be used in multiple threads. Only Dispose can be called in any thread. /// /// /// UI elements are implemented and live in their applications. This class just communicates with them. /// [Known UI element issues in various applications](xref:ui_element_issues) /// /// /// /// Click link "Example" in Chrome. /// /// Click a link, wait for new web page, click a link in it. /// /// [StructLayout(LayoutKind.Sequential)] public unsafe sealed partial class elm : IDisposable { //FUTURE: elm.more.EnableElmInChromeWebPagesWhenItStarts //FUTURE: elm.more.EnableElmInJavaWindows (see JavaEnableJAB in QM2) //FUTURE: add functions to marshal to another thread. internal struct Misc_ { public EMiscFlags flags; public byte roleByte; //for optimization. 0 if not set or failed to get. 0xFF (ERole.Custom) if VT_BSTR or not 1-ROLE_MAX. public ushort level; //for ToString. 0 if not set. public void SetRole(ERole role) { this.roleByte = (byte)(role <= 0 || role > ERole.TREEBUTTON ? ERole.Custom : role); } public void SetLevel(int level) { this.level = (ushort)Math.Clamp(level, 0, 0xffff); } } internal IntPtr _iacc; internal int _elem; internal Misc_ _misc; //Real elm object memory size with header: 32 bytes on 64-bit. //We don't use RCW, which would add another 32 bytes. /// /// Creates elm from IAccessible and child id. /// By default does not AddRef. /// iacc must not be 0. /// internal elm(IntPtr iacc, int elem = 0, bool addRef = false) { _Set(iacc, elem, default, addRef); } /// /// Creates from Cpp_Acc. /// By default does not AddRef. /// x.acc must not be 0. /// internal elm(Cpp.Cpp_Acc x, bool addRef = false) { _Set(x.acc, x.elem, x.misc, addRef); } /// /// Sets fields. /// _iacc must be 0, iacc not 0. /// void _Set(IntPtr iacc, int elem = 0, Misc_ misc = default, bool addRef = false) { Debug.Assert(_iacc == default); Debug.Assert(iacc != default); if (addRef) Marshal.AddRef(iacc); _iacc = iacc; _elem = elem; _misc = misc; int mp = _MemoryPressure; GC.AddMemoryPressure(mp); //s_dmp += mp; if(s_dmp > DebugMaxMemoryPressure) DebugMaxMemoryPressure = s_dmp; //DebugMemorySum += mp; } int _MemoryPressure => _elem == 0 ? c_memoryPressure : c_memoryPressure / 10; const int c_memoryPressure = 1000; //assume this is the average UI element memory size in both processes //internal static int DebugMaxMemoryPressure; //static int s_dmp; //internal static int DebugMemorySum; /// void Dispose(bool disposing) { //print.it(disposing); if (_iacc != default) { var t = _iacc; _iacc = default; //perf.first(); Marshal.Release(t); //perf.nw(); //print.it($"rel: {t} {Marshal.Release(t)}"); int mp = _MemoryPressure; GC.RemoveMemoryPressure(mp); //s_dmp -= mp; } _elem = 0; _misc = default; } /// /// Releases COM object and clears this variable. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// ~elm() { #if DEBUG //print.it("~elm"); try { Dispose(false); } catch (Exception ex) { print.it(ex); } #else Dispose(false); #endif } //TODO2: try: release elm COM objects in process.Exit event. Use weak reference. Or GC.Collect. //internal void Debug1() { // print.it(_iacc, Debug_.GetComObjRefCount_(_iacc)); //} //not used in this library, but sometimes may need it for testing something in scripts. internal IntPtr Iacc_ => _iacc; /// /// Gets or changes simple element id, also known as child id. /// /// /// Most UI elements are not simple elements. Then this property is 0. /// Often (but not always) this property is the 1-based item index in parent. For example LISTITEM in LIST. /// The set function sometimes can be used as a fast alternative to . It modifies only this variable. It does not check whether the value is valid. /// Simple elements cannot have child elements. /// public int Item { get => _elem; set { _misc.roleByte = 0; _elem = value; } } /// /// Returns some additional info about this variable, such as how the UI element was retrieved (inproc, UIA, Java). /// public EMiscFlags MiscFlags => _misc.flags; /// /// Gets or sets indentation level for . /// /// /// When or similar function finds a UI element, it sets this property of the variable. If etc, it is 0 (unknown). /// When searching in a window, at level 0 are direct children of the WINDOW. When searching in controls (specified class or id), at level 0 is the control. When searching in , at level 0 are its direct children. When searching in web page (role prefix "web:" etc), at level 0 is the web page (role DOCUMENT or PANE). /// public int Level { get => _misc.level; set => _misc.SetLevel(value); } /// /// Returns true if this variable is disposed. /// bool _Disposed => _iacc == default; internal void ThrowIfDisposed_() { if (_Disposed) throw new ObjectDisposedException(nameof(elm)); WarnInSendMessage_(); } internal static void WarnInSendMessage_() { if (Api.InSendMessageBlocked) { //as fast as TickCount64 long t = Environment.TickCount64; if (t - s_wismTime > 500) { s_wismTime = t; print.warning("elm functions may not work here. This code is called by another thread through SendMessage or is a low-level hook. Try workarounds: call ReplyMessage (Windows API); use an asynchronous way to call the elm function (timer, Dispatcher.InvokeAsync, etc); let that thread instead of SendMessage use an asynchronous function such as PostMessage."); } } } static long s_wismTime; /// /// Gets UI element of window or control. Or some its standard part - client area, titlebar etc. /// /// Window or control. /// Window part id. Default EObjid.WINDOW. Also can be a custom id supported by that window, cast int to EObjid. /// Flags. /// Invalid window. /// Failed. For example, window of a higher [](xref:uac) integrity level process. /// objid is QUERYCLASSNAMEIDX or NATIVEOM. /// /// Uses API AccessibleObjectFromWindow. /// public static elm fromWindow(wnd w, EObjid objid = EObjid.WINDOW, EWFlags flags = 0) { WarnInSendMessage_(); bool spec = false; switch (objid) { case EObjid.QUERYCLASSNAMEIDX: //use WM_GETOBJECT case EObjid.NATIVEOM: //use API AccessibleObjectFromWindow throw new ArgumentException(); case EObjid.CARET: //w should be 0 case EObjid.CURSOR: //w should be 0 case EObjid.ALERT: //only with AccessibleObjectFromEvent? case EObjid.SOUND: //only with AccessibleObjectFromEvent? spec = true; flags |= EWFlags.NotInProc; break; } var hr = Cpp.Cpp_AccFromWindow(flags.Has(EWFlags.NotInProc) ? 1 : 0, w, objid, out var a, out _); if (hr != 0) { if (flags.Has(EWFlags.NoThrow)) return null; if (spec && w.Is0) throw new AuException(); w.ThrowIfInvalid(); _WndThrow(hr, w, "*get UI element from window."); } return new elm(a); } static void _WndThrow(int hr, wnd w, string es) { w.UacCheckAndThrow_(es); throw new AuException(hr, es); } /// /// Gets UI element from point. /// /// Returns null if failed. Usually fails if the window is of a higher [](xref:uac) integrity level process. With some windows can fail occasionally. /// /// Coordinates. /// Tip: To specify coordinates relative to the right, bottom, work area or a non-primary screen, use , like in the example. /// /// /// /// Get UI element at 100 200. /// /// /// Get UI element at 50 from left and 100 from the bottom edge of the work area. /// /// public static elm fromXY(POINT p, EXYFlags flags = 0) { WarnInSendMessage_(); return fromXY_(p, flags); } //used by fromXY and Delm internal static elm fromXY_(POINT p, EXYFlags flags, Cpp.Cpp_AccFromPointCallbackT callback = null) { wnd w = default; bool retry = false; gRetry: int hr = Cpp.Cpp_AccFromPoint(p, flags, (flags, wFP, wTL) => { w = wFP; if (callback is { } cb) flags = cb(flags, wFP, wTL); if (osVersion.minWin8_1 ? !flags.Has(EXYFlags.NotInProc) : flags.Has(EXYFlags.UIA)) { bool dpiV = Dpi.IsWindowVirtualized(wTL); if (dpiV) flags |= Enum_.EXYFlags_DpiScaled; } return flags; }, out var a); if (hr != 0) return null; //workaround for Chrome bug. Sometimes DOCUMENT. Eg after scrolling. Also in some pages or page parts, eg LA home page when scrolled to the bottom. Same in all modes except acc notinproc. if (!retry && a.misc.roleByte == (byte)ERole.DOCUMENT && w.ClassNameIs("Chrome*")) { Marshal.Release(a.acc); retry = true; goto gRetry; } return new elm(a); } /// public static elm fromXY(int x, int y, EXYFlags flags = 0) => fromXY(new POINT(x, y), flags); //rejected: FromXY(Coord, Coord, ...). Coord makes no sense. /// /// Gets UI element from mouse cursor (pointer) position. /// /// /// Failed. For example, window of a higher [](xref:uac) integrity level process. /// /// Uses API AccessibleObjectFromPoint. /// public static elm fromMouse(EXYFlags flags = 0) { return fromXY(mouse.xy, flags); } /// /// Gets the keyboard-focused UI element. /// /// null if failed. public static elm focused(EFocusedFlags flags = 0) { WarnInSendMessage_(); var w = wnd.focused; g1: if (w.Is0) return null; int hr = Cpp.Cpp_AccGetFocused(w, flags, out var a); if (hr != 0) { var w2 = wnd.focused; if (w2 != w) { w = w2; goto g1; } return null; } return new elm(a); } /// /// Gets the UI element that generated the event that is currently being processed by the callback function used with API SetWinEventHook or . /// /// null if failed. Supports . /// /// /// /// /// The parameters are of the callback function. /// Uses API AccessibleObjectFromEvent. /// Often fails because the UI element already does not exist, because the callback function is called asynchronously, especially when the event is OBJECT_DESTROY, OBJECT_HIDE, SYSTEM_xEND. /// Returns null if failed. Always check the return value, to avoid . An exception in the callback function kills this process. /// public static elm fromEvent(wnd w, EObjid idObject, int idChild) { int hr = Api.AccessibleObjectFromEvent(w, idObject, idChild, out var iacc, out var v); if (hr == 0 && iacc == default) hr = Api.E_FAIL; if (hr != 0) { lastError.code = hr; return null; } int elem = v.vt == Api.VARENUM.VT_I4 ? v.ValueInt : 0; return new elm(iacc, elem); } #if false //rejected: not useful. Maybe in the future. /// /// Gets UI element from a COM object of any type that supports it. /// /// null if failed. /// Unmanaged COM object. /// /// The COM object type can be IAccessible, IAccessible2, IHTMLElement, ISimpleDOMNode or any other COM interface type that can give IAccessible interface pointer through API IUnknown.QueryInterface or IServiceProvider.QueryService. /// For IHTMLElement and ISimpleDOMNode returns null if the HTML element is not an accessible object. Then you can try to get UI element of its parent HTML element, parent's parent and so on, until succeeds. /// public static elm fromComObject(IntPtr x) { if(x == default) return null; if(MarshalUtil.QueryInterface(x, out IntPtr iacc, Api.IID_IAccessible) || MarshalUtil.QueryService(x, out iacc, Api.IID_IAccessible) ) return new elm(iacc); return null; } /// /// Gets UI element from a COM object of any type that supports it. /// Returns null if failed. /// /// Managed COM object. /// /// The COM object type can be IAccessible, IAccessible2, IHTMLElement, ISimpleDOMNode or any other COM interface type that can give IAccessible interface pointer through API IUnknown.QueryInterface or IServiceProvider.QueryService. /// For IHTMLElement and ISimpleDOMNode returns null if the HTML element is not an accessible object. Then you can try to get UI element of its parent HTML element, parent's parent and so on, until succeeds. /// public static elm fromComObject(object x) { if(x == null) return null; //FUTURE: support UIA. Don't use LegacyIAccessible, it work not with all windows. Instead wrap in UIAccessible. //if(x is UIA.IElement e) { //info: IElement2-7 are IElement too // var pat = e.GetCurrentPattern(UIA.PatternId.LegacyIAccessible) as UIA.ILegacyIAccessiblePattern; // x = pat?.GetIAccessible(); // if(x == null) return null; //} var ip = Marshal.GetIUnknownForObject(x); if(ip == default) return null; try { return FromComObject(ip); } finally { Marshal.Release(ip); } } #endif /// /// Used only for debug. /// enum _FuncId { name = 1, value, description, default_action, role, state, rectangle, parent_object, child_object, container_window, child_count, child_objects, help_text, keyboard_shortcut, html, selection, uiaid, uiacn } /// /// Calls SetLastError and returns hr. /// In Debug config also outputs error in red. /// If hr looks like not an error but just the property or action is unavailable, changes it to S_FALSE and does not show error. These are: S_FALSE, DISP_E_MEMBERNOTFOUND, E_NOTIMPL. /// _FuncId also can be char, like (_FuncId)'n' for name. /// int _Hresult(_FuncId funcId, int hr) { if (hr != 0) { switch (hr) { case Api.DISP_E_MEMBERNOTFOUND: case Api.E_NOTIMPL: hr = Api.S_FALSE; break; case (int)Cpp.EError.InvalidParameter: throw new ArgumentException("Invalid argument value."); default: Debug.Assert(!Cpp.IsCppError(hr)); break; } #if DEBUG if (hr != Api.S_FALSE) { _DebugPropGet(funcId, hr); } #endif } lastError.code = hr; return hr; } #if DEBUG void _DebugPropGet(_FuncId funcId, int hr) { if (t_debugNoRecurse || _Disposed) return; if (funcId >= (_FuncId)'A') { switch ((char)funcId) { case 'R': funcId = _FuncId.role; break; case 'n': funcId = _FuncId.name; break; case 'v': funcId = _FuncId.value; break; case 'd': funcId = _FuncId.description; break; case 'h': funcId = _FuncId.help_text; break; case 'a': funcId = _FuncId.default_action; break; case 'k': funcId = _FuncId.keyboard_shortcut; break; case 's': funcId = _FuncId.state; break; case 'r': funcId = _FuncId.rectangle; break; case 'u': funcId = _FuncId.uiaid; break; case 'U': funcId = _FuncId.uiacn; break; } } if (hr == Api.E_FAIL && funcId == _FuncId.default_action) return; //many in old VS etc t_debugNoRecurse = true; try { var s = ToString(); print.it($"<>-{funcId}, 0x{hr:X} - {lastError.messageFor(hr)} {s}"); } finally { t_debugNoRecurse = false; } } [ThreadStatic] static bool t_debugNoRecurse; #endif /// /// Formats string from main properties of this UI element. /// /// /// The string starts with role. Other properties have format like x="value", where x is a property character like with ; character e is . HTML attributes have format @name="value". In string values are used C# escape sequences, for example \r\n for new line. /// Indentation depends on . /// public override string ToString() { if (_Disposed) return ""; if (!GetProperties("Rnsvdarw@", out var k)) return ""; using (new StringBuilder_(out var b)) { if (Level > 0) b.Append(' ', Level); b.Append(k.Role); _Add('n', k.Name); if (k.State != 0) _Add('s', k.State.ToString(), '(', ')'); _Add('v', k.Value); _Add('d', k.Description); _Add('a', k.DefaultAction); if (!k.Rect.Is0) _Add('r', k.Rect.ToString(), '\0', '\0'); if (Item != 0) b.Append(", e=").Append(Item); foreach (var kv in k.HtmlAttributes) { b.Append(", @").Append(kv.Key).Append('=').Append('"'); b.Append(kv.Value.Escape(limit: 250)).Append('"'); } _Add('w', k.WndContainer.ClassName ?? ""); void _Add(char name, string value, char q1 = '"', char q2 = '"') { if (value.Length == 0) return; var t = value; if (q1 == '"') t = t.Escape(limit: 250); b.Append(", ").Append(name).Append('='); if (q1 != '\0') b.Append(q1); b.Append(t); if (q1 != '\0') b.Append(q2); } return b.ToString(); } } } ================================================ FILE: Au/UI objects/elmFinder.cs ================================================ namespace Au; /// /// Finds UI elements (). Contains name and other parameters of elements to find. /// /// /// Find link "Example" in web page, and click. Wait max 5 s. Exception if not found. /// /// Find window that contains certain UI element, and get the UI element too. /// f.In(t).Exists()); //or t => t.HasElm(f) /// print.it(w); /// print.it(f.Result); /// ]]> /// public unsafe class elmFinder { readonly string _role, _name, _prop, _navig; readonly EFFlags _flags; readonly int _skip; readonly Func _also; Cpp.Cpp_AccFindCallbackT _also2; elmFinder _next; char _resultProp; wnd _wnd; elm _elm; /// /// The found UI element. /// null if not found or if used . /// public elm Result { get; private set; } /// /// The requested property of the found UI element, depending on . /// null if: 1. UI element not found. 2. ResultGetProperty not used or is '-'. 3. Failed to get the property. /// /// /// The type depends on the property. Most properties are of type String. Others: , , , . /// public object ResultProperty { get; private set; } /// /// Set this when you need only some property of the UI element (name, etc) and not the UI element itself. /// The value is a character like with , for example 'n' for Name. Use '-' if you don't need any property. /// /// Used parameter also, navig or next. public char ResultGetProperty { set { if (_also != null) throw new ArgumentException("ResultGetProperty cannot be used with parameter 'also'."); if (_navig != null) throw new ArgumentException("ResultGetProperty cannot be used with parameter 'navig'."); if (_next != null) throw new ArgumentException("ResultGetProperty cannot be used with a path finder."); _resultProp = value; } } void _ClearResult() { Result = null; ResultProperty = null; } /// /// Stores the specified UI element properties in this object. /// /// public elmFinder(string role = null, [ParamString(PSFormat.Wildex)] string name = null, Strings prop = default, EFFlags flags = 0, Func also = null, int skip = 0, string navig = null ) { _role = role; _name = name; _prop = prop.Value switch { null => null, string s => s.Replace('|', '\0'), _ => string.Join('\0', prop.ToArray()) }; _flags = flags; _also = also; _skip = skip >= -1 ? skip : throw new ArgumentOutOfRangeException(nameof(skip)); _navig = navig; } internal elmFinder(wnd w, elm e) { _wnd = w; _elm = e; _flags = _EFFlags_Empty; } const EFFlags _EFFlags_Empty = (EFFlags)0x10000000; /// /// Creates an for finding a UI element. Supports path like var e = w.Elm["ROLE1", "Name1"]["ROLE2", "Name2"].Find();. /// /// The new finder or the first finder in path. /// /// UI element role (), like "LINK". /// Can have prefix "web:", "firefox:" or "chrome:" which means "search only in web page" and enables Chrome UI elements. /// Case-sensitive. Not wildcard. null means "can be any". Cannot be "". /// More info in Remarks. /// /// /// UI element name (). /// String format: [wildcard expression](xref:wildcard_expression). /// null means "any". "" means "empty or unavailable". /// /// /// Other UI element properties and search settings. /// Examples: "value=xxx|@href=yyy", new("value=xxx", "@href=yyy"). /// More info in Remarks. /// /// /// /// Callback function. Called for each matching UI element. Let it return true if this is the wanted UI element. /// Example: the UI element must contain point x y: o => o.GetRect(out var r, o.WndTopLevel) && r.Contains(266, 33) /// /// /// 0-based index of matching UI element to use. Will skip this number of matching elements. /// Value -1 means "any", and can be useful when this finder is intermediate (ie not the last) in a path or when it has navig. If intermediate, will search for next element in all matching intermediate elements. If has navig, will retry with other matching elements if fails to navigate in the first found. It is slower and not so often useful, therefore the default value of this parameter is 0, not -1. /// Cannot be used with , unless it is not in the last part of path. /// /// If not null, after finding the specified UI element will call with this string and use its result instead of the found element. /// flags contains UIA or ClientArea when appending (only the first finder can have these flags). /// /// To create code for this function, use tool Find UI element. /// /// In [wildcard expression](xref:wildcard_expression) supports PCRE regular expressions (prefix "**r ") but not .NET regular expressions (prefix "**R "). They are similar. /// /// When using path like ["ROLE1", "Name1"]["ROLE2", "Name2"]["ROLE3", "Name3"], multiple finders are linked like finder1 -> finder2 -> finder3, so that the chain of finders will find UI element specified by the last finder. /// /// More info in topic. /// ///
About the role parameter
/// /// Can be standard role (see ) like "LINK" or custom role like "div". See . /// /// Can have a prefix: /// - "web:" - search only in the visible web page, not in whole window. Example: "web:LINK".\ /// Supports Chrome, Firefox, Internet Explorer (IE) and apps that use same code (Edge, Opera...). With other windows, searches in the first found visible UI element that has DOCUMENT role.\ /// Tip: To search only NOT in web pages, use prop "notin=DOCUMENT" (Chrome, Firefox) or "notin=PANE" (IE). /// - "firefox:" - search only in the visible web page of Firefox or Firefox-based web browser. If w window class name starts with "Mozilla", can be used "web:" instead. /// - "chrome:" - search only in the visible web page of Chrome or Chrome-based web browser. If w window class name starts with "Chrome", can be used "web:" instead. /// /// Chrome web page UI elements normally are disabled (don't exist). Use prefix "web:" or "chrome:" to enable. /// /// Prefix cannot be used: /// - if prop contains "id" or "class"; /// - with flags UIA, ClientArea; /// - when searching in . /// ///
About the prop parameter
/// /// Format: one or more "name=value" strings, like new("key=xxx", "@href=yyy") or "key=xxx|@href=yyy". Names must match case. Values of most string properties are [wildcard expressions](xref:wildcard_expression). /// /// - "class" - search only in child controls that have this class name (see ).\ /// Cannot be used when searching in a UI element. /// - "id" - search only in child controls that have this id (see ). If the value is not a number - Windows Forms control name (see ); case-sensitive, not wildcard.\ /// Cannot be used when searching in a UI element. /// - "value" - . /// - "desc" - . /// - "state" - . List of states the UI element must have and/or not have.\ /// Example: "state=CHECKED, FOCUSABLE, !DISABLED".\ /// Example: "state=0x100010, !0x1".\ /// Will find UI element that has all states without "!" prefix and does not have any of states with "!" prefix. /// - "rect" - with raw true. Can be specified left, top, width and/or height, using format.\ /// Example: "rect={L=1155 T=1182 W=132 H=13}". /// Example: "rect={W=132 T=1182}". /// The L T coordinates are relative to the primary screen. /// - "level" - level (see ) at which the UI element can be found. Can be exact level, or minimal and maximal level separated by space.\ /// The default value is 0 1000. /// - "item" - . /// - "action" - . /// - "key" - . /// - "help" - . /// - "uiaid" - . /// - "uiacn" - . /// - "url" - URL of the container DOCUMENT in Chromium-based web browser. Used together with role prefix "web:" or "chrome:". Use when the document is a side panel or developer tools or a special page like Settings. Don't need (but can be used too) when the document is a web page (URL starts with "https:", "http:" or "file:"). /// - "maxcc" - when searching, skip children of UI elements that have more than this number of direct children. Default 10000, min 1, max 1000000.\ /// It can make faster. It also prevents hanging or crashing when a UI element in the UI element tree has large number of children. For example OpenOffice Calc TABLE has one billion children. /// - "notin" - when searching, skip children of UI elements that have these roles. It can make faster.\ /// Example: "notin=TREE,LIST,TOOLBAR".\ /// Roles in the list must be separated with "," or ", ". Case-sensitive, not wildcard. See also: . /// - "@attr" - . Here "attr" is any attribute name. Example: "@href=example". ///
/// /// public elmFinder this[string role = null, [ParamString(PSFormat.Wildex)] string name = null, Strings prop = default, EFFlags flags = 0, Func also = null, int skip = 0, string navig = null ] { get { var f = new elmFinder(role, name, prop, flags, also, skip, navig); if (_flags == _EFFlags_Empty) { f._wnd = _wnd; f._elm = _elm; return f; } _Last().Next = f; return this; } } //rejected: default skip = -1. Rarely need. // In some cases could find an unexpected element. Better to not find than to find (and click etc) wrong element. // Instead the tool gives info to try -1 when not found or found wrong element. /// /// Gets or sets next finder in path (immediately after this finder). /// /// flags contains UIA or ClientArea. /// /// The setter creates or modifies a path (a chain of linked finders). Unlike , which appends to the last finder in path, this function appends to this finder. /// public elmFinder Next { get => _next; set { if (_flags.Has(_EFFlags_Empty)) throw new InvalidOperationException(); if (value != _next) { if (value != null) { if (value._flags.HasAny(EFFlags.UIA | EFFlags.ClientArea)) throw new ArgumentException("Don't use flags UIA and ClientArea when searching in elm."); } _next = value; _also2 = null; } } } elmFinder _Last() { var n = this; while (n._next != null) n = n._next; return n; } /// /// Sets or changes window or control where etc will search. /// /// This. /// public elmFinder In(wnd w) { if (_flags.Has(_EFFlags_Empty)) throw new InvalidOperationException(); _wnd = w; return this; } /// /// Sets or changes parent UI element where etc will search. /// /// This. /// public elmFinder In(elm e) { if (_flags.Has(_EFFlags_Empty)) throw new InvalidOperationException(); _elm = e; return this; } /// /// Finds the first matching descendant UI element in the window or UI element. /// /// If found, returns , else null. /// /// - role is "" or invalid. /// - name is invalid wildcard expression ("**options " or regular expression). /// - prop contains unknown property names or errors in wildcard expressions. /// - navig string is invalid. /// - flags has UIA or ClientArea when searching in web page (role prefix "web:" etc) or . /// - role has a prefix ("web:" etc) when searching in . /// - not 0 when searching in . /// /// Invalid window handle (0 or closed). See also . /// Failed. For example, window of a higher [](xref:uac) integrity level process. /// /// To create code for this function, use tool Find UI element. /// /// More info in topic. /// /// /// Find link "Example" in web page, and click. Wait max 5 s. Throw if not found. /// /// Try to find link "Example" in web page. Return if not found. Click if found. /// /// public elm Find() => Exists() ? Result : null; /// /// Finds the first matching descendant UI element in the window or UI element. Can wait and throw . /// /// If found, returns . Else throws exception or returns null (if wait negative). /// The wait timeout, seconds. If 0, does not wait. If negative, does not throw exception when not found. /// /// public elm Find(Seconds wait) => Exists(wait) ? Result : null; /// /// Finds the first matching descendant UI element in the window or UI element. Like , just different return type. /// /// If found, sets and returns true, else false. /// public bool Exists() => Find_(_elm != null, _wnd, _elm); /// /// Finds the first matching descendant UI element in the window or UI element. Can wait and throw . Like , just different return type. /// /// If found, sets and returns true. Else throws exception or returns false (if wait negative). /// public bool Exists(Seconds wait) { if (Find_(_elm != null, _wnd, _elm, wait.Exists_() ? null : wait)) return true; return wait.ReturnFalseOrThrowNotFound_(); } /// /// Waits for a matching descendant UI element to appear in the window or UI element. /// /// If found, returns . On timeout returns null if timeout is negative; else exception. /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// /// /// Same as , except: /// - 0 timeout means infinite. /// - on timeout throws , not . /// /// public elm Wait(Seconds timeout) => Find_(_elm != null, _wnd, _elm, timeout) ? Result : null; //CONSIDER: public bool WaitNot(Seconds timeout) => wait.until(timeout, () => !Exists()); internal bool Find_(bool inElm, wnd w, elm eParent, Seconds? waitS = null, bool isNext = false, bool fromFindAll = false) { if (_flags.Has(_EFFlags_Empty) && !fromFindAll) throw new InvalidOperationException(); if (_flags.Has(EFFlags.UIA | EFFlags.ClientArea)) throw new ArgumentException("Don't use flags UIA and ClientArea together."); if (_role != null) { if (_role == "") throw new ArgumentException("role cannot be \"\"."); if (inElm && 0 != _role.Starts(false, "web:", "chrome:", "firefox:")) throw new ArgumentException("Don't use role prefix when searching in elm."); } Cpp.Cpp_Acc aParent; Cpp.Cpp_Acc* pParent = null; EFFlags flags = _flags; if (inElm) { if (!isNext) { if (_flags.HasAny(EFFlags.UIA | EFFlags.ClientArea)) throw new ArgumentException("Don't use flags UIA and ClientArea when searching in elm."); if (eParent == null) throw new ArgumentNullException(); eParent.ThrowIfDisposed_(); if (eParent.Item != 0) throw new ArgumentException("Item not 0."); } aParent = new(eParent); pParent = &aParent; if (!eParent.MiscFlags.Has(EMiscFlags.InProc)) flags |= EFFlags.NotInProc; } else { w.ThrowIfInvalid(); aParent = default; } elm.WarnInSendMessage_(); bool inProc = !flags.Has(EFFlags.NotInProc); _ClearResult(); var ap = new Cpp.Cpp_AccFindParams(_role, _name, _prop, flags, Math.Max(0, _skip), _resultProp); if (!inElm) ap.RolePrefix(w); //converts role prefix to flags (C++ internal) and may enable Chrome AOs //if used skip<0 and path or navig, need to search in all possible paths. For it we use the 'also' callback. //FUTURE: optimize. Add part of code to the C++ dll. Now can walk same tree branches multiple times. Cpp.Cpp_AccFindCallbackT also = null; bool allPaths = _skip < 0 && (_next != null || _navig != null); bool findAll = t_findAll?.ContainsKey(this) == true; //in FindAll. This is the last finder in path. if (allPaths) { also = _also2 ??= (ca, _) => { var e = new elm(ca); if (!_AlsoNavigNext(ref e)) return 0; Result = e; return 1; }; } else if (findAll) { also = (ca, _) => { var e = new elm(ca); _AlsoNavigNext(ref e); return 0; }; } else { if (_also != null) also = _also2 ??= (ca, _) => _also(new elm(ca)) ? 1 : 0; } Seconds seconds = waitS ?? new(-1); seconds.Period ??= inProc ? 10 : 40; var loop = new WaitLoop(seconds); for (bool doneUAC = false, doneThread = false; ;) { var hr = Cpp.Cpp_AccFind(w, pParent, ap, also, out var ca, out string sResult); if (findAll) { GC.KeepAlive(also); if (hr == Cpp.EError.NotFound) return false; //else error. Cannot be 0. } if (hr == 0) { switch (_resultProp) { case '\0': if (!allPaths) { var e = new elm(ca); if (_AlsoNavigNext(ref e, noAlso: true)) Result = e; else hr = Cpp.EError.NotFound; } break; case 'r' or 'D' or 's' or 'w' or '@': if (sResult == null) break; unsafe { fixed (char* p = sResult) { switch (_resultProp) { case 'r' or 'D': ResultProperty = *(RECT*)p; break; case 's': ResultProperty = *(EState*)p; break; case 'w': ResultProperty = (wnd)(*(int*)p); break; case '@': ResultProperty = elm.AttributesToDictionary_(p, sResult.Length); break; } } } break; default: ResultProperty = sResult; break; } if (hr == 0) return true; } if (hr == Cpp.EError.InvalidParameter) throw new ArgumentException(sResult); if ((hr == Cpp.EError.WindowClosed) || (!w.Is0 && !w.IsAlive)) return false; //FUTURE: check if a is disconnected etc. Or then never wait. if (!doneUAC) { doneUAC = true; w.UacCheckAndThrow_(); //CONSIDER: don't throw. Maybe show warning. } //print.it(hr > 0 ? $"hr={hr}" : $"hr={(int)hr:X}"); if (hr == Cpp.EError.NotFound) { if (waitS == null) return false; } else { Debug.Assert(!Cpp.IsCppError((int)hr)); if (hr == (Cpp.EError)Api.RPC_E_SERVER_CANTMARSHAL_DATA && !_flags.Has(EFFlags.NotInProc)) throw new AuException((int)hr, "For this UI element need flag NotInProc"); throw new AuException((int)hr); } if (!doneThread) { doneThread = true; if (!w.Is0 && w.IsOfThisThread) return false; } if (!loop.Sleep()) return false; GC.KeepAlive(eParent); } } bool _AlsoNavigNext(ref elm e, bool noAlso = false) { if (!noAlso && _also != null && !_also(e)) return false; if (_navig != null) { var e2 = e.Navigate(_navig); if (e2 == null) return false; if (t_navigResult.need) t_navigResult = (true, e, e2); e = e2; } if (_next != null) { if (e.Item != 0) return false; if (!_next.Find_(true, default, e, isNext: true)) return false; e = _next.Result; } else if (t_findAll?.TryGetValue(this, out var a) == true) { a.Add(e); return false; } return true; } /// /// Finds all matching descendant UI elements in the window or UI element. /// /// Array of 0 or more elements. /// /// /// /// See . /// public elm[] FindAll() { var a = new List(); var last = _Last(); if (last._skip != 0) throw new ArgumentException("FindAll does not support *skip* in the last part of path"); (t_findAll ??= new()).Add(last, a); try { Find_(_elm != null, _wnd, _elm, fromFindAll: true); } finally { t_findAll.Remove(last); } return a.ToArray(); } //This dictionary is used to temporarily attach the List that collects FindAll results to the elmFinder. // Another way - add a field to elmFinder and somehow make it thread-safe. But elmFinder is immutable. [ThreadStatic] static Dictionary> t_findAll; internal static List<(elm, RECT)> GetAllWithRect_(wnd w, EFFlags flags) { List<(elm, RECT)> a = []; Cpp.Cpp_AccFindCallbackT also = (ca, rect) => { var e = new elm(ca); a.Add((e, *rect)); return 0; }; var ap = new Cpp.Cpp_AccFindParams(null, null, null, flags, 0, default); Cpp.Cpp_AccFind(w, null, ap, also, out var ca, out string sResult, getRects: true); GC.KeepAlive(also); return a; //TODO2: try async. Let the in-proc code return immediately and later post results. // Alternatively can use CoMarshalInterThreadInterfaceInStream, but it makes ~50% slower if need to marshal all. Maybe can optimize to marshal only some. //TODO2: add `elm.MarshalToThisThread`. //TODO2: (heavy change) maybe instead of `IAccessible` use a UIA element. Wrap acc in it like now uia in acc. // Why: UIA elements are free-threaded (unlike acc). // Why: maybe then easier to wrap more UIA API. // Or some way to avoid passing COM objects to this process. } /// public override string ToString() { using (new StringBuilder_(out var b)) { for (var n = this; n != null; n = n._next) n._ToString(b); return b.ToString(); } } void _ToString(StringBuilder b) { b.Append('['); int n = 0; _Add(0, null, _role); _Add(1, "name", _name); _Add(2, "prop", _prop?.Replace('\0', '|')); if (_flags != 0) _Add(3, "flags", _flags.ToString(), false); if (_also != null) _Add(4, "also", "...", false); if (_skip != 0) _Add(5, "skip", _skip.ToString(), false); _Add(6, "navig", _navig); b.Append(']'); void _Add(int i, string name, string value, bool isString = true) { if (value == null) return; if (n > 0) b.Append(", "); if (i > n++) b.Append(name).Append(": "); if (isString) value = value.Escape(limit: 50, quote: true); b.Append(value); } } [ThreadStatic] internal static (bool need, elm before, elm after) t_navigResult; //rejected. Rarely used. Maybe in the future, but different API, maybe ...In(controls).Find(), and move the code into _Find. ///// ///// Finds UI element in the specified control of window w. ///// ///// If found, returns , else null. ///// Window that contains the control. ///// Control properties. This functions searches in all matching controls. ///// Exceptions of . ///// ///// Functions Find and Exists differ only in their return types. ///// ///// Alternatively you can specify control class name or id in role. How this function is different: 1. Allows to specify more control properties. 2. Works better/faster when the control is of a different process or thread than the parent window; else slightly slower. ///// //public elm Find(wnd w, wndChildFinder controls) => Exists(w, controls) ? Result : null; ///// If found, sets and returns true, else false. ///// //public bool Exists(wnd w, wndChildFinder controls) { // w.ThrowIfInvalid(); // foreach (var c in controls.FindAll(w)) { // try { // if (_Find(false, c, null)) { // controls.Result = c; // return true; // } // } // catch (AuException ex) when (!c.IsAlive) { Debug_.Print(ex); } //don't throw AuWndException/AuException if the window or a control is destroyed while searching, but throw AuException if eg access denied // } // return false; //} //rejected. Better slightly longer code than unclear and possibly ambiguous code where you have to learn string parsing rules. //public static implicit operator elmFinder(string roleNameProp) { // if (roleNameProp.NE()) throw new ArgumentException(); // int i = roleNameProp.FindAny(",\0"); // if (i < 0) return new(roleNameProp); // var a = roleNameProp.Split(roleNameProp[i], 3); // return new(a[0].Length > 0 ? a[0] : null, a[1], a.Length < 3 ? null : a[2]); //} } partial class elm { /// /// Gets an for finding UI elements in a window or UI element that can be set later with . /// Example: var e = elm.path["ROLE", "Name"].In(w).Find();. Same as var e = w.Elm["ROLE", "Name"].Find();. /// Example: var e = elm.path["ROLE1", "Name1"]["ROLE2", "Name2"]["ROLE3", "Name3"].In(w).Find();. /// /// public static elmFinder path { get; } = new(default, null); /// /// Gets an for finding UI elements in this UI element. /// Example: var e2 = e1.Elm["ROLE", "Name"].Find();. /// Example: var e2 = e1.Elm["ROLE1", "Name1"]["ROLE2", "Name2"]["ROLE3", "Name3"].Find();. /// Example: print.it(e.Elm.FindAll());. /// public elmFinder Elm => new(default, this); } public partial struct wnd { /// /// Gets an for finding UI elements in this window or control. /// Example: var e = w.Elm["ROLE", "Name"].Find();. /// Example: var e = w.Elm["ROLE1", "Name1"]["ROLE2", "Name2"]["ROLE3", "Name3"].Find();. /// Example: print.it(w.Elm.FindAll());. /// public elmFinder Elm => new(this, null); //public static wndFinder f_( // [ParamString(PSFormat.wildex)] string name = null, // [ParamString(PSFormat.wildex)] string cn = null, // [ParamString(PSFormat.wildex)] WOwner of = default, // WFlags flags = 0, Func also = null, WContains contains = default // ) => new(name, cn, of, flags, also, contains); //public wnd this[ // [ParamString(PSFormat.wildex)] string name = null, // [ParamString(PSFormat.wildex)] string cn = null, // int? id = null, WCFlags flags = 0, Func also = null, int skip = 0, // ] { // get => Child(name, cn, k, also, skip); //} } ================================================ FILE: Au/UI objects/elm_func.cs ================================================ //FUTURE: ChildFromXY. Like wnd.ChildFromXY. Use IAccessible.accHitTest. namespace Au { public unsafe partial class elm { /// /// Gets the container window or control of this UI element. /// /// default(wnd) if failed. Supports . /// /// All UI elements must support this property, but some have bugs and can fail or return a wrong window. /// Uses API WindowFromAccessibleObject. /// public wnd WndContainer { get { ThrowIfDisposed_(); _Hresult(_FuncId.container_window, _GetWnd(out var w)); return w; } } /// /// Low-level version of . Does not call ThrowIfDisposed_ and _Hresult (lastError). /// /// HRESULT int _GetWnd(out wnd w) { int hr = Cpp.Cpp_AccGetInt(this, 'w', out var i); GC.KeepAlive(this); w = (wnd)i; return hr; } /// /// Gets the top-level window that contains this UI element. /// /// default(wnd) if failed. Supports . /// /// All UI elements must support this property, but some have bugs and can return default(wnd). /// Uses API WindowFromAccessibleObject and API GetAncestor. /// public wnd WndTopLevel => WndContainer.Window; //note: named not WndWindow, to avoid using accidentally instead of WndContainer. /// /// Gets location of this UI element in screen. /// /// Empty rectangle if failed or this property is unavailable. Supports . /// /// Calls . /// Most but not all UI elements support this property. /// public RECT Rect { get { GetRect(out var r); return r; } } internal RECT RectRawDpi_ { get { GetRect(out var r, true); return r; } } /// /// Gets location of this UI element in screen. /// /// false if failed or this property is unavailable. Supports . /// Rectangle in screen coordinates. /// /// Don't DPI-scale. When the element is in a DPI-scaled/virtualized window (see ), the raw rectangle may not match the visible rectangle. /// This parameter is ignored on Windows 7 and 8.0 or if this element was retrieved not in-process. /// /// /// Most but not all UI elements support this property. /// public bool GetRect(out RECT r, bool raw = false) { ThrowIfDisposed_(); if (!raw && MiscFlags.Has(EMiscFlags.InProc) && osVersion.minWin8_1) { if (!GetProperties("D", out var p)) { r = default; return false; } r = p.Rect; } else { var hr = _Hresult(_FuncId.rectangle, Cpp.Cpp_AccGetRect(this, out r)); GC.KeepAlive(this); if (hr != 0) return false; } return true; } /// /// Gets location of this UI element in the client area of window w. /// /// false if failed or this property is unavailable. Supports . /// Receives rectangle in w client area coordinates. /// Window or control. /// Intersect the rectangle with the w client area, possibly making it smaller or empty. /// /// Most but not all UI elements support this property. /// Uses and . /// public bool GetRect(out RECT r, wnd w, bool intersect = false) { if (!(GetRect(out r) && w.MapScreenToClient(ref r))) return false; if (intersect) r.Intersect(_GetContainerClientRect(w)); return true; RECT _GetContainerClientRect(wnd w) { var rc = w.ClientRect; //if w is a classic listview in report view, exclude header if (Item > 0 && RoleInt == ERole.LISTITEM) { var h = w.ChildFast(null, "SysHeader32"); if (h.IsVisible) rc.top = h.Rect.Height; } return rc; } } /// /// Gets role as enum . /// /// 0 (ERole.None) if failed. Supports . /// /// Most UI elements have a standard role, defined in enum (except None and Custom). Some UI elements have a custom role, usually as string; then returns ERole.Custom. /// All UI elements must support this property. If failed, probably the is invalid, for example the window is closed. /// public ERole RoleInt { get { ThrowIfDisposed_(); if (_misc.roleByte != 0) return (ERole)_misc.roleByte; _Hresult(_FuncId.role, _GetRole(out var role, out _, dontNeedStr: true)); //Debug_.Print("roleByte 0 -> " + role + ", " + Role); //it's OK in some cases, eg when retrieved not inproc, or after navigating or changing simpleelementid return role; } } /// /// Like , but returns 0 if int role not available (does not go to get it). /// internal ERole RoleInt_ => (ERole)_misc.roleByte; /// /// Gets standard or custom role, as string. /// /// "" if failed. Supports . /// /// Most UI elements have a standard role, defined in enum (except None and Custom). Some UI elements have a custom role, usually as string. /// For standard roles this function returns enum member name. For string roles - the string. For unknown non-string roles - the int value like "0" or "500". /// All UI elements must support this property. If failed, probably the is invalid, for example the window is closed. /// public string Role { get { ThrowIfDisposed_(); var role = (ERole)_misc.roleByte; if (role is 0 or ERole.Custom) { if (0 != _Hresult(_FuncId.role, _GetRole(out role, out var roleStr, dontNeedStr: false))) return ""; if (roleStr != null) return roleStr; } var a = s_roles; uint u = (uint)role; return (u < a.Length) ? a[u] : ((int)role).ToString(); } } static readonly string[] s_roles = { "0", "TITLEBAR", "MENUBAR", "SCROLLBAR", "GRIP", "SOUND", "CURSOR", "CARET", "ALERT", "WINDOW", "CLIENT", "MENUPOPUP", "MENUITEM", "TOOLTIP", "APPLICATION", "DOCUMENT", "PANE", "CHART", "DIALOG", "BORDER", "GROUPING", "SEPARATOR", "TOOLBAR", "STATUSBAR", "TABLE", "COLUMNHEADER", "ROWHEADER", "COLUMN", "ROW", "CELL", "LINK", "HELPBALLOON", "CHARACTER", "LIST", "LISTITEM", "TREE", "TREEITEM", "PAGETAB", "PROPERTYPAGE", "INDICATOR", "IMAGE", "STATICTEXT", "TEXT", "BUTTON", "CHECKBOX", "RADIOBUTTON", "COMBOBOX", "DROPLIST", "PROGRESSBAR", "DIAL", "HOTKEYFIELD", "SLIDER", "SPINBUTTON", "DIAGRAM", "ANIMATION", "EQUATION", "BUTTONDROPDOWN", "BUTTONMENU", "BUTTONDROPDOWNGRID", "WHITESPACE", "PAGETABLIST", "CLOCK", "SPLITBUTTON", "IPADDRESS", "TREEBUTTON" }; //Returns HRESULT. int _GetRole(out ERole roleInt, out string roleStr, bool dontNeedStr) { roleStr = null; Debug.Assert((ERole)_misc.roleByte is 0 or ERole.Custom); var hr = Cpp.Cpp_AccGetRole(this, out roleInt, out var b); if (hr == 0) { if (!b.Is0) { roleInt = ERole.Custom; if (dontNeedStr) b.Dispose(); else roleStr = b.ToStringAndDispose(); } _misc.SetRole(roleInt); } return hr; } int _GetState(out EState state) { int hr = Cpp.Cpp_AccGetInt(this, 's', out int i); GC.KeepAlive(this); state = (EState)i; return hr; } /// /// Gets UI element state (flags). /// /// 0 if failed. Supports . /// /// /// public EState State { get { ThrowIfDisposed_(); _Hresult(_FuncId.state, _GetState(out var state)); return state; } } /// Calls and returns true if has state CHECKED. public bool IsChecked => State.Has(EState.CHECKED); /// Calls and returns true if has state CHECKED, null if has state MIXED, else false. Use this function with 3-state checkboxes. public bool? IsChecked2 => (State & (EState.CHECKED | EState.MIXED)) switch { EState.CHECKED => true, 0 => false, _ => null }; /// Calls and returns true if has state UNAVAILABLE. /// Does not check whether this UI element is in a disabled parent/ancestor UI element. public bool IsDisabled => State.Has(EState.DISABLED); /// Calls and returns true if has state FOCUSED. public bool IsFocused => State.Has(EState.FOCUSED); /// Calls and returns true if has state INVISIBLE and does not have state OFFSCREEN. /// /// If the UI element has both INVISIBLE and OFFSCREEN states, it is either invisible or just offscreen, depending on application etc. Then this function works like and similar functions: for most UI elements returns false (is visible), but for UI elements that have these roles returns true (invisible): WINDOW, DOCUMENT, PROPERTYPAGE, GROUPING, ALERT, MENUPOPUP. /// Does not check whether this UI element is in an invisible parent/ancestor UI element. /// public bool IsInvisible => IsInvisible_(State); internal bool IsInvisible_(EState state) { if (!state.Has(EState.INVISIBLE)) return false; if (!state.Has(EState.OFFSCREEN)) return true; switch (RoleInt) { case ERole.WINDOW: case ERole.DOCUMENT: case ERole.PROPERTYPAGE: case ERole.GROUPING: case ERole.ALERT: case ERole.MENUPOPUP: return true; //note: these roles must be the same as in _IsRoleToSkipIfInvisible in "acc find.cpp" } return false; } /// Calls and returns true if has state OFFSCREEN. public bool IsOffscreen => State.Has(EState.OFFSCREEN); /// Calls and returns true if has state PROTECTED. /// This state is used for password fields. public bool IsPassword => State.Has(EState.PROTECTED); /// Calls and returns true if has state PRESSED. public bool IsPressed => State.Has(EState.PRESSED); /// Calls and returns true if has state READONLY. public bool IsReadonly => State.Has(EState.READONLY); /// Calls and returns true if has state SELECTED. public bool IsSelected => State.Has(EState.SELECTED); /// /// Converts BSTR to string and disposes the BSTR. /// If hr is not 0, returns "" (never null). /// static string _BstrToString(int hr, BSTR b) { if (hr == 0) return b.ToStringAndDispose() ?? ""; return ""; } string _GetStringProp(char prop) { ThrowIfDisposed_(); int hr = Cpp.Cpp_AccGetStringProp(this, prop, out var b); GC.KeepAlive(this); var s = _BstrToString(hr, b); _Hresult((_FuncId)prop, hr); return s; } static string _GetStringPropL(Cpp.Cpp_Acc a, char prop) { int hr = Cpp.Cpp_AccGetStringProp(a, prop, out var b); return _BstrToString(hr, b); } /// /// Gets name. /// /// "" if name is unavailable or if failed. Supports . /// /// UI element name usually is its read-only text (eg button text, link text), or its adjacent read-only text (eg text label by this edit box). It usually does not change, therefore can be used to find or identify the UI element. /// public string Name { get => _GetStringProp('n'); //note: bug of standard toolbar buttons: fails to get name if all these are true: // This process is 64-bit and the target process is 32-bit. // Button Name property comes from its tooltip. // Found not inproc, eg with flag NotInProc. } /// /// Gets of window/control w. /// Returns null if w invalid. Returns "" if failed to get name. /// internal static string NameOfWindow_(wnd w) { if (!w.IsAlive) return null; var hr = Cpp.Cpp_AccFromWindow(1 | 2, w, 0, out _, out var b); return _BstrToString(hr, b); //speed: inproc ~10% faster. But first time slower, especially if process of different bitness. } /// /// Gets or sets value. /// /// Failed to set value. /// /// Usually it is editable text or some other value that can be changed at run time, therefore in most cases it cannot be used to find or identify the UI element reliably. /// The get function returns "" if this property is unavailable or if failed. Supports . /// Most UI elements don't support set. /// public string Value { get => _GetStringProp('v'); set { ThrowIfDisposed_(); AuException.ThrowIfHresultNot0(_InvokeL('v', value)); } } /// /// Gets description. /// /// "" if this property is unavailable or if failed. Supports . public string Description { get => _GetStringProp('d'); } /// /// Gets help text. /// /// "" if this property is unavailable or if failed. Supports . public string Help { get => _GetStringProp('h'); } /// /// Gets the UI Automation AutomationId property. /// /// "" if this property is unavailable or if failed. Supports . /// /// Only UI elements found with flag can have this property. /// public string UiaId { get => _GetStringProp('u'); } /// /// Gets the UI Automation ClassName property. /// /// "" if this property is unavailable or if failed. Supports . /// /// Only UI elements found with flag can have this property. /// public string UiaCN { get => _GetStringProp('U'); } /// /// Gets keyboard shortcut. /// /// "" if this property is unavailable or if failed. Supports . public string KeyboardShortcut { get => _GetStringProp('k'); } /// /// Gets default action of . /// /// "" if this property is unavailable or if failed. Supports . /// /// If this is a Java UI element, returns all actions that can be used with , like "action1, action2, action3", from which the first is considered default and is used by . /// Note: the string is supposed to be localized, ie depends on UI language; except Java. /// public string DefaultAction { get => _GetStringProp('a'); } /// /// Performs the UI element's default action (see ). Usually it is "click", "press" or similar. Like a mouse click but without moving the mouse cursor or pressing mouse buttons. /// /// Failed. /// /// Fails if the UI element does not have a default action. Then you can use , or try , , , other functions. /// The action can take long time, for example show a dialog. This function normally does not wait. It allows the caller to automate the dialog. If it waits, try or one of the above functions ( etc). /// public void Invoke() { ThrowIfDisposed_(); _Invoke(); } /// /// Calls _InvokeL, with ButtonPostClickWorkaround_ if need. Exception if failed. /// void _Invoke(char action = 'a', string param = null, string errMsg = null) { int hr; if (!MiscFlags.HasAny(EMiscFlags.UIA | EMiscFlags.Java) && RoleInt is ERole.BUTTON or ERole.SPLITBUTTON or ERole.CHECKBOX or ERole.RADIOBUTTON) { using var workaround = new mouse.ButtonPostClickWorkaround_(WndContainer); hr = _InvokeL(action, param); } else { hr = _InvokeL(action, param); } AuException.ThrowIfHresultNot0(hr, errMsg); //_MinimalSleep(); //don't need. It does not make more reliable. } /// /// Calls EnableActivate(-1) and Cpp_AccAction. /// /// HRESULT int _InvokeL(char action = 'a', string param = null) { //UIA bug: if window inactive, in some cases tries to activate, and waits ~10 s if failed. // Eg ExpandCollapse pattern (always) and Invoke/Toggle patterns (buttons/checkboxes, not always). // Non-UIA servers also could try to activate, although now I don't remember such cases. WndUtil.EnableActivate(-1); //usually quite fast, and often faster than WndContainer int hr = Cpp.Cpp_AccAction(this, action, param); GC.KeepAlive(this); return hr; } //void _MinimalSleep() //{ // Thread.Sleep(15); // //if(0 == _iacc.GetWnd(out var w)) w.MinimalSleepIfOtherThread_(); //better don't call getwnd //} /// /// Performs one of actions supported by this Java UI element. /// /// /// Action name. See . /// If null (default), performs default action (like ) or posts Space key message. More info in Remarks. /// Failed. /// /// Read more about Java UI elements in topic. /// /// Problem: if the action opens a dialog, / do not return until the dialog is closed (or fail after some time). The caller then waits and cannot automate the dialog. Also then this process cannot exit until the dialog is closed. If the action parameter is null and the UI element is focusable, this function tries a workaround: it makes the UI element (button etc) focused and posts Space key message, which should press the button; then this function does not wait. /// public void JavaInvoke(string action = null) { //problem: if the button click action opens a modal dialog, doAccessibleActions waits until closed. // Waits 8 s and then returns true. Somehow in QM2 returns false. // During that time any JAB calls (probably from another thread) are blocked and fail. Tried various combinations. // Also then releaseJavaObject does not return until the dialog closed. It even does not allow our process to exit. In QM2 the same. // Previously (with old Java version?) then whole JAB crashed. Now doesn't. Or crashes only in some conditions that I now cannot reproduce. ThrowIfDisposed_(); if (action == null && 0 == _GetState(out var state) && (state & (EState.FOCUSABLE | EState.SELECTABLE)) == EState.FOCUSABLE //must be only focusable. If SELECTABLE, probably don't need this workaround. && 0 == _GetWnd(out var w) && 0 == Cpp.Cpp_AccSelect(this, ESelect.TAKEFOCUS) && 0 == _GetState(out state) && state.Has(EState.FOCUSED) //avoid sending keys to another control ) { GC.KeepAlive(this); w.Post(Api.WM_KEYDOWN, (byte)KKey.Space, 0); w.Post(Api.WM_KEYUP, (byte)KKey.Space, 0); //tested: works even if the window is inactive. w.MinimalSleepNoCheckThread_(); return; } _Invoke('a', action); //_MinimalSleep(); //don't need. JAB doAccessibleActions is sync, which is bad. } /// /// Calls or action and waits until changes the web page (window name and page name). /// /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// Default 60 seconds. /// /// If used, calls it instead of . /// Returns true. On timeout returns false if timeout < 0; else exception. /// timeout time has expired (if > 0). /// Failed. For example, when this UI element is invalid, or its top-level window does not contain a web page. /// The window was closed while waiting. /// Exceptions thrown by or by the action function. /// /// This function is used to click a link in a web page and wait until current web page is gone. It prevents a following "wait for UI element" function from finding a matching UI element in the old page, which would be bad. /// Does not wait until the new page is completely loaded. There is no reliable/universal way for it. Instead, after calling it you can call a "wait for UI element" function which waits for a known UI element that must be in the new page. /// This function cannot be used when the new page has the same title as current page. Then it waits until timeout time or forever. The same if the invoked action does not open a web page. /// public bool WebInvoke(Seconds? timeout = null, Action action = null) { wnd w = WndTopLevel; if (w.Is0) throw new AuException("*get window"); elm doc = w.Elm["web:"].Find(-1) ?? throw new AuException("*find web page"); string wndName = w.NameTL_, docName = doc.Name; Debug.Assert(!wndName.NE() && !docName.NE()); bool wndOK = false, docOK = false; elmFinder f = null; if (action == null) Invoke(); else action(this); //wait until window name and document name both are changed. They can change in any order. var seconds = timeout ?? new(60); seconds.Period ??= 25; var loop = new WaitLoop(seconds); while (loop.Sleep()) { w.ThrowIfInvalid(); if (!wndOK) { var s = w.NameTL_; if (s == null) continue; //probably invalid, will throw in next loop wndOK = s != wndName; } if (wndOK && !docOK) { f ??= new elmFinder("web:") { ResultGetProperty = 'n' }; if (!w.HasElm(f)) continue; //eg in Firefox for some time there is no DOCUMENT if (f.ResultProperty is not string s) continue; docOK = s != docName; //if(!docOK) print.it("doc is late"); } if (wndOK && docOK) { w.ThrowIfInvalid(); return true; } } return false; } #if false //This function is finished and normally works well. //However web browsers not always fire the event. For some pages never, or only when not cached. //Also, some pages are like never finish loading (the browser waiting animation does not stop spinning). Or finish when the wanted UI element is there for long time, so why to wait. Or finish, then continue loading again... //Also, this function inevitably will stop working with some new web browser version with new bugs. Too unreliable. public bool InvokeAndWaitForWebPageLoaded(double secondsTimeout = 60, Action action = null, wnd w = default) { ThrowIfDisposed_(); int timeout; bool throwTimeout = false; if(secondsTimeout == 0) timeout = Timeout.Infinite; else { if(secondsTimeout < 0) secondsTimeout = -secondsTimeout; else throwTimeout = true; timeout = checked((int)(secondsTimeout * 1000)); } if(w.Is0) { w = WndContainer; if(w.Is0) throw new AuException("*get window"); } var hookEvent = EEvent.IA2_DOCUMENT_LOAD_COMPLETE; int browser = w.ClassNameIs(Api.string_IES, "Mozilla*", "Chrome*"); switch(browser) { case 0: wnd ies = w.Child(null, Api.string_IES); if(ies.Is0) break; w = ies; goto case 1; case 1: hookEvent = EEvent.OBJECT_CREATE; break; } int tid = w.ThreadId; if(tid == 0) w.ThrowUseNative(); AutoResetEvent eventNotify = null; int debugIndex = 0; Api.WINEVENTPROC hook = (IntPtr hWinEventHook, EEvent ev, wnd hwnd, int idObject, int idChild, int idEventThread, int time) => { if(eventNotify == null) { /*print.it("null 1");*/ return; } if(ev == EEvent.OBJECT_CREATE && hwnd != w) return; //note: in Chrome hwnd is Chrome_RenderWidgetHostHWND int di = ++debugIndex; using(var a = elm.fromEvent(hwnd, idObject, idChild)) { if(a == null) { /*Debug_.Print("elm.fromEvent null");*/ return; } //often IE, but these are not useful UI elements if(eventNotify == null) { /*print.it("null 2");*/ return; } if(ev == EEvent.IA2_DOCUMENT_LOAD_COMPLETE) { //Chrome, Firefox //filter out frame/iframe if(browser == 2) { //Firefox does not fire events for frame/iframe. But the Chrome code would work too. } else if(0 == a._iacc.get_accParent(out var a2)) { //bug in some Chrome versions: fails for main document using(a2) { if(eventNotify == null) { /*print.it("null 3");*/ return; } bool isFrame; var hr = a2.GetRole(0, out var role, out var roleStr); if(hr != 0) Debug_.Print((uint)hr); if(eventNotify == null) { /*print.it("null 4");*/ return; } if(hr != 0) isFrame = false; else if(roleStr != null) isFrame = roleStr.Ends("frame", true); else isFrame = !(role == ERole.WINDOW || role == ERole.CLIENT); //print.it(role, roleStr); if(isFrame) return; //browser main frame iframe //Firefox "browser" "frame" "iframe" //Chrome WINDOW "FRAME" DOCUMENT } } } else { //IE (OBJECT_CREATE) if(a._elem != 0) return; if(0 != a._iacc.GetRole(0, out var role) || role != ERole.PANE) return; if(eventNotify == null) { /*print.it("null 3");*/ return; } //filter out frame/iframe if(a.IsInvisible) return; if(eventNotify == null) { /*print.it("null 4");*/ return; } using(var aCLIENT = _FromWindow(w, EObjid.CLIENT, noThrow: true)) { if(eventNotify == null) { /*print.it("null 5");*/ return; } if(aCLIENT != null) { var URL1 = a.Value; Debug.Assert(URL1.Length > 0); //print.it(URL1); //http:..., about:... aCLIENT.get_accName(0, out var URL2, 0); Debug.Assert(URL2.Length > 0); if(URL1 != URL2) return; if(eventNotify == null) { /*print.it("null 6");*/ return; } } else Debug_.Print("aCLIENT null"); } } //print.it(di, ev, a); eventNotify.Set(); eventNotify = null; } }; var hh = Api.SetWinEventHook(hookEvent, hookEvent, default, hook, 0, tid, 0); if(hh == default) throw new AuException(); try { eventNotify = new AutoResetEvent(false); if(action != null) action(this); else Invoke(); if(eventNotify.WaitOne(timeout)) { //Thread.CurrentThread.Join(2000); return true; } } finally { Api.UnhookWinEvent(hh); eventNotify?.Dispose(); } GC.KeepAlive(hook); if(throwTimeout) throw new TimeoutException(); return false; } #endif /// /// Selects or deselects. /// /// Specifies whether to select, focus, add to selection etc. Can be two flags, for example ESelect.TAKEFOCUS | ESelect.TAKESELECTION. With flag TAKEFOCUS activates the window like . /// Failed. /// Failed to activate the window () or focus the control (). /// /// Not all UI elements support it. Most UI elements support not all flags. It depends on FOCUSABLE, SELECTABLE, MULTISELECTABLE, EXTSELECTABLE, DISABLED. /// Many UI elements have bugs, especially with flag TAKEFOCUS. More bugs when the UI element has been found with flag . /// public void Select(ESelect how = ESelect.TAKESELECTION) { ThrowIfDisposed_(); //Workaround for Windows controls bugs, part 1. wnd w = default, wTL = default; bool focusingControl = false; if (how.Has(ESelect.TAKEFOCUS)) { //Always activate/focus the window, because used by functions that then will send keys etc. //CONSIDER: option TAKEFOCUSNOACTIVATE to focus without activate/focus the window. w = WndContainer; //if(!w.IsEnabled(true)) throw new AuException("*set focus. Disabled"); //accSelect would not fail //rejected. In some cases the UI element may be focusable although window disabled, eg KTreeView. wTL = w.Window; wTL.Activate(); if (focusingControl = (w != wTL)) if (w.IsEnabled()) //see above. Would be exception if disabled. w.Focus(); if (IsFocused) how &= ~ESelect.TAKEFOCUS; if (how == 0) return; } else { //same as with Invoke WndUtil.EnableActivate(-1); } for (int i = 0; i < 2; i++) { var hr = Cpp.Cpp_AccSelect(this, how); GC.KeepAlive(this); if (hr == 0) break; if (hr == 1) continue; //some UI elements return S_FALSE even if did what asked. Eg combobox (focuses the child Edit), slider. Or may need to retry, eg when trying to focus a listitem in a non-focused listbox. if (hr == Api.DISP_E_MEMBERNOTFOUND) throw new AuException("This UI element does not support this state"); AuException.ThrowIfHresultNegative(hr); } if (!w.Is0) w.MinimalSleepIfOtherThread_(); //sleep only when focusing. Assume selection is sync. Also need for the bug, because the control may be activated a millisecond later. //Workaround for Windows controls bugs, part 2. if (focusingControl && w.IsActive) { //Debug_.Print("activated control"); wTL.ActivateL(); } //tested: IAccessible.accSelect(TAKEFOCUS): // Most Windows controls have this bug: activates the control with SetForegroundWindow, which deactivates the top-level window. // Especially if the control is already focused. // If not already focused, fails if eg listbox item. But then works well with eg buttons. // MSDN: If IAccessible::accSelect is called with the SELFLAG_TAKEFOCUS flag on a child UI element that has an HWND, the flag takes effect only if the UI element's parent has the focus. // Tested, does not help: LockSetForegroundWindow, AttachThreadInput. // Good news: works well if the UI element found inproc, ie without flag NotInproc. // But then need to focus the control, else does not work. // Use the same workaround. It focuses the control. // Works well with web browsers, WinForms. // With WPF initially almost does not work. After using a navigation key (Tab etc) starts to work well. //tested: UIA.IElement.SetFocus: // In most cases works well with standard controls, all web browsers, WinForms. // With WPF same as elm. // Bug: If standard control is disabled, deactivates parent window and draws focus rectangle on the control. } /// /// Makes this UI element focused for keyboard input. /// /// Add flag TAKESELECTION. Note: it is for selecting a list item, not for selecting text in a text box. /// Failed. /// Failed to activate the window () or focus the control (). /// /// Calls with flag TAKEFOCUS and optionally TAKESELECTION. /// Not all UI elements support this action and not all work correctly. More info in Select documentation. /// public void Focus(bool andSelect = false) { var how = ESelect.TAKEFOCUS; if (andSelect) how |= ESelect.TAKESELECTION; Select(how); } /// /// Gets selected direct child items. /// /// Empty array if there are no selected items of if failed. Supports . public elm[] SelectedChildren { get { ThrowIfDisposed_(); if (_elem != 0) { lastError.clear(); return []; } //return _iacc.get_accSelection(); if (0 != _Hresult(_FuncId.selection, Cpp.Cpp_AccGetSelection(this, out var b)) || b.Is0) return []; GC.KeepAlive(this); var p = (Cpp.Cpp_Acc*)b.Ptr; int n = b.Length / sizeof(Cpp.Cpp_Acc); var r = new elm[n]; for (int i = 0; i < n; i++) r[i] = new elm(p[i]); b.Dispose(); return r; } } /// /// Gets the number of direct child UI elements. /// public int ChildCount { get { ThrowIfDisposed_(); if (_elem != 0) { lastError.clear(); return 0; } _Hresult(_FuncId.child_count, Cpp.Cpp_AccGetInt(this, 'c', out int cc)); GC.KeepAlive(this); return cc; } } /// /// Gets multiple properties. /// /// /// String that specifies properties to get, for example "nv" for name and value. ///
R - . ///
n - . ///
v - . ///
d - . ///
h - . ///
a - . ///
k - . ///
u - . ///
U - . ///
s - . ///
r - with raw true. ///
D - or with raw false. Don't use with r. ///
w - . ///
o - outer. ///
i - inner. ///
@ - . /// /// Receives results. /// false if failed, for example when the UI element's window is closed. Supports . /// Unknown property character. /// /// The returned variable contains values of properties specified in props. When a property is empty or failed to get, the member variable is "", empty dictionary or default value of that type; never null. /// /// Normally this function is faster than calling multiple property functions, because it makes single remote procedure call. But not if this UI element was found with flag etc. /// public bool GetProperties(string props, out EProperties result) { //CONSIDER: use cached role. Or not, because now can help to catch bugs where the cached role is incorrect. result = null; ThrowIfDisposed_(); if (props.Length == 0) return true; int hr = Cpp.Cpp_AccGetProps(this, props, out var b); GC.KeepAlive(this); if (hr != 0) { if (hr == (int)Cpp.EError.InvalidParameter) throw new ArgumentException("Unknown property character."); lastError.code = hr; return false; } result = new(); using (b) { var offsets = (int*)b.Ptr; for (int i = 0; i < props.Length; i++) { int offs = offsets[i], len = ((i == props.Length - 1) ? b.Length : offsets[i + 1]) - offs; var p = b.Ptr + offs; switch (props[i]) { case 'r' or 'D': result.Rect = len > 0 ? *(RECT*)p : default; break; case 's': result.State = len > 0 ? *(EState*)p : default; break; case 'w': result.WndContainer = len > 0 ? (wnd)(*(int*)p) : default; break; case '@': result.HtmlAttributes = AttributesToDictionary_(p, len); break; default: var s = (len == 0) ? "" : new string(p, 0, len); switch (props[i]) { case 'R': result.Role = s; break; case 'n': result.Name = s; break; case 'v': result.Value = s; break; case 'd': result.Description = s; break; case 'h': result.Help = s; break; case 'a': result.DefaultAction = s; break; case 'k': result.KeyboardShortcut = s; break; case 'u': result.UiaId = s; break; case 'U': result.UiaCN = s; break; case 'o': result.OuterHtml = s; break; case 'i': result.InnerHtml = s; break; } break; } } } return true; } internal static Dictionary AttributesToDictionary_(char* p, int len) { var d = new Dictionary(); int ik = 0, iv = 0; for (int i = 0; i < len; i++) { var c = p[i]; if (c == '\0' && iv > ik) { string sk = new string(p, ik, iv - ik - 1); string sv = new string(p, iv, i - iv); d[sk] = sv; ik = i + 1; } else if (c == '=' && iv <= ik) { iv = i + 1; } } //print.it(d); return d; } /// /// Gets an adjacent or related UI element - next, child, parent, etc. /// /// null if not found. /// /// String consisting of one or more navigation direction strings separated by space, like "parent next child4 first". ///
"next" - next sibling UI element in the same parent UI element. ///
"previous" - previous sibling UI element in the same parent UI element. ///
"first" - first child UI element. ///
"last" - last child UI element. ///
"parent" - parent (container) UI element. ///
"child" - child UI element by 1-based index. Example: "child3" (3-th child). Negative index means from end, for example -1 is the last child. ///
"#N" - custom. More info in Remarks. ///
• Some elements also support "up", "down", "left", "right". /// /// Wait for the wanted UI element max this number of seconds. Default 0 (don't wait). If negative, waits forever. /// Invalid navig string. /// /// Can be 2 letters, like "pr" for "previous". /// A string like "next3" or "next,3" is the same as "next next next". Except for "child". /// Use string like "#1000" to specify a custom navDir value to pass to IAccessible.accNavigate. /// /// For "child" the function calls API AccessibleChildren. /// For "parent" the function calls IAccessible.get_accParent. Few UI elements don't support. Some UI elements return a different parent than in the tree of UI elements. /// For others the function calls IAccessible.accNavigate. Not all UI elements support it. Some UI elements skip invisible siblings. Instead you can use "parent childN" or "childN". /// /// /// /// public elm Navigate(string navig, double waitS = 0) { Not_.Null(navig); ThrowIfDisposed_(); int hr; Cpp.Cpp_Acc ca; if (waitS == 0) { hr = Cpp.Cpp_AccNavigate(this, navig, out ca); } else { var loop = new WaitLoop(waitS > 0 ? -waitS : 0d); do hr = Cpp.Cpp_AccNavigate(this, navig, out ca); while (hr != 0 && hr != (int)Cpp.EError.InvalidParameter && loop.Sleep()); } GC.KeepAlive(this); if (hr == (int)Cpp.EError.InvalidParameter) throw new ArgumentException("Invalid navig string."); lastError.code = hr; return hr == 0 ? new elm(ca) : null; //FUTURE: when fails, possibly this is disconnected etc. Retry find with same elmFinder. } /// /// Gets parent element. Same as with argument "pa". /// /// null if failed. public elm Parent => Navigate("pa"); //info: Navigate("pa") is optimized in C++ //Chrome bug: the parent element retrieved in this way has some incorrect properties. // Eg Parent.WndContainer is the legacy control, whereas this.WndContainer is the top-level window. /// /// Gets HTML. /// /// "" if this is not a HTML element or if failed. Supports . /// If true, gets outer HTML (with tag and attributes), else inner HTML. /// /// Works with Chrome, Internet Explorer and apps that use their code (Edge, Opera, web browser controls...). This UI element must be found without flag NotInProc. /// If this is the root of web page (role DOCUMENT or PANE), gets web page body HTML. /// public string Html(bool outer) { ThrowIfDisposed_(); int hr = _Hresult(_FuncId.html, Cpp.Cpp_AccWeb(this, outer ? "'o" : "'i", out BSTR s)); GC.KeepAlive(this); return _BstrToString(hr, s); } /// /// Gets a HTML attribute. /// /// "" if this is not a HTML element or does not have the specified attribute or failed. Supports . /// Attribute name, for example "href", "id", "class". Full, case-sensitive. /// /// Works with Chrome, Internet Explorer and apps that use their code (Edge, Opera, web browser controls...). This UI element must be found without flag NotInProc. /// /// name is null/""/invalid. public string HtmlAttribute(string name) { ThrowIfDisposed_(); if (name.NE() || name[0] == '\'') throw new ArgumentException("Invalid name."); int hr = _Hresult(_FuncId.html, Cpp.Cpp_AccWeb(this, name, out BSTR s)); GC.KeepAlive(this); return _BstrToString(hr, s); } /// /// Gets all HTML attributes. /// /// Empty dictionary if this is not a HTML element or does not have attributes or failed. Supports . /// /// Works with Chrome, Internet Explorer and apps that use their code (Edge, Opera, web browser controls...). This UI element must be found without flag NotInProc. /// public Dictionary HtmlAttributes() { ThrowIfDisposed_(); int hr = Cpp.Cpp_AccWeb(this, "'a", out BSTR s); GC.KeepAlive(this); _Hresult(_FuncId.html, hr); if (hr != 0) return new(); using (s) return AttributesToDictionary_(s.Ptr, s.Length); } /// /// Scrolls this UI element into view. /// /// Failed to scroll, or the UI element does not support scrolling. /// /// This function works with these UI elements: /// - Web page elements in Chrome, Internet Explorer and apps that use their code (Edge, Opera, web browser controls...). To find UI element, use role prefix "web:", "firefox:" or "chrome:", and don't use flag . /// - Standard treeview, listview and listbox controls. /// - Some other controls if found with flag . /// /// Some apps after scrolling update with a delay. Some apps never update it for existing variables. This function does not wait. /// public void ScrollTo() { ThrowIfDisposed_(); AuException.ThrowIfHresultNot0(_ScrollTo(), "*scroll"); } int _ScrollTo() { int hr = 1; if (MiscFlags.Has(EMiscFlags.UIA)) { hr = _InvokeL('s'); } else if (Item == 0) { hr = Cpp.Cpp_AccWeb(this, "'s", out _); //tested: Chrome and Firefox don't support UI Automation scrolling (IUIAutomationScrollItemPattern). } else if (RoleInt is ERole.LISTITEM or ERole.TREEITEM) { //try messages of some standard controls var w = WndContainer; switch (w.CommonControlType) { case WControlType.Listview when RoleInt is ERole.LISTITEM: if (0 != w.Send(LVM_ENSUREVISIBLE, Item - 1, 1)) hr = 0; break; case WControlType.Listbox when RoleInt is ERole.LISTITEM: if (-1 != w.Send(LB_SETTOPINDEX, Item - 1)) hr = 0; break; case WControlType.Treeview when RoleInt is ERole.TREEITEM: if (osVersion.is32BitProcessAnd64BitOS && !w.Is32Bit) break; //cannot get 64-bit HTREEITEM nint hi = w.Send(TVM_MAPACCIDTOHTREEITEM, Item); if (hi == 0) break; w.Send(TVM_ENSUREVISIBLE, 0, hi); hr = 0; //the API returns nonzero only if actually scrolls, not if already visible break; } } GC.KeepAlive(this); return hr; } /// /// Waits for a user-defined state/condition of this UI element. For example enabled, checked, changed name. /// /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// Callback function (eg lambda). It is called repeatedly, until returns a value other than default(T), for example true. /// Returns the value returned by the callback function. On timeout returns default(T) if timeout is negative; else exception. /// timeout time has expired (if > 0). /// Failed to get container window (), or it was closed while waiting. public T WaitFor(Seconds timeout, Func condition) { var w = WndContainer; //calls ThrowIfDisposed_ var loop = new WaitLoop(timeout); for (; ; ) { w.ThrowIfInvalid(); T r = condition(this); bool ok = !EqualityComparer.Default.Equals(r, default); w.ThrowIfInvalid(); //eg when waiting for button enabled, if window closed while in callback, the DISABLED state may be removed if (ok) return r; if (!loop.Sleep()) return default; } } /// /// Moves the cursor (mouse pointer) to this UI element. /// /// X coordinate in the bounding rectangle of this UI element. Default - center. Examples: 10, ^10 (reverse), .5f (fraction). /// Y coordinate in the bounding rectangle of this UI element. Default - center. /// If not 0, the function at first calls . If it succeeds, waits scroll number of milliseconds (let the target app update the UI element rectangle etc). /// Failed to get UI element rectangle in container window (). /// Exceptions of . /// /// Calls . To get rectangle in window, uses with intersect true. /// public void MouseMove(Coord x = default, Coord y = default, int scroll = 0) => _ElmMouseAction(false, x, y, default, scroll); /// /// Clicks this UI element. /// /// X coordinate in the bounding rectangle of this UI element. Default - center. Examples: 10, ^10 (reverse), .5f (fraction). /// Y coordinate in the bounding rectangle of this UI element. Default - center. /// Which button and how to use it. /// If not 0, the function at first calls . If it succeeds, waits scroll number of milliseconds (let the target app update the UI element rectangle etc). Valid values are 0-5000. Tip: if does not scroll, try to find the UI element with flag UIA. /// Failed to get UI element rectangle in container window (). /// Exceptions of . /// /// Calls . To get rectangle in window, uses with intersect true. /// public MRelease MouseClick(Coord x = default, Coord y = default, MButton button = MButton.Left, int scroll = 0) { _ElmMouseAction(true, x, y, button, scroll); return button; } /// /// Double-clicks this UI element. /// /// public void MouseClickD(Coord x = default, Coord y = default, int scroll = 0) => MouseClick(x, y, MButton.DoubleClick, scroll); /// /// Right-clicks this UI element. /// /// public void MouseClickR(Coord x = default, Coord y = default, int scroll = 0) => MouseClick(x, y, MButton.Right, scroll); void _ElmMouseAction(bool click, Coord x, Coord y, MButton button, int scroll) { var (w, r) = _GetWndAndRectForClick(scroll); var p = Coord.NormalizeInRect(x, y, r, centerIfEmpty: true); //if (!w.Is0) { if (button == 0) mouse.move(w, p.x, p.y); else mouse.clickEx(button, w, p.x, p.y); //} else { //no. Unsafe, can click in another window. // if (button == 0) mouse.move(p); // else mouse.clickEx(button, p); //} } (wnd w, RECT r) _GetWndAndRectForClick(int scroll) { if (scroll != 0) { if ((uint)scroll > 5000) throw new ArgumentException("Valid values 0-5000", nameof(scroll)); if (0 == _ScrollTo()) wait.ms(scroll); } if (!GetRect(out var r)) throw new AuException(0, "*get UI element rectangle"); if (r.NoArea) throw new AuException(IsOffscreen ? "The UI element is offscreen. Try scroll." : "The UI element rectangle is empty"); //var w = WndContainer; //need window for mouse functions, else could click another window etc var w = WndTopLevel; //direct parent control may be zero-size etc, eg in Win11 Paint when found with UIA bool retry = false; var r0 = r; g1: if (!w.GetRect(out var rw)) throw new AuException(0, "*get container window"); if (!r.Intersect(rw)) { if (!retry && scroll != 0) { //workaround for: WndContainer of a popup item may be the popup's owner. Eg WPF combobox. w = w.Window.Get.EnabledOwned(); if (retry = !w.Is0) { r = r0; goto g1; } } throw new AuException("The UI element rectangle is not in the container window." + (scroll != 0 ? null : " Try scroll.")); } if (!w.MapScreenToClient(ref r)) throw new AuException(0); return (w, r); } //rejected: automatically scroll if need. // Impossible to reliably detect whether need to scroll. // This was an attempt, but it does not work well. And can't click non-client elements. //(wnd w, RECT r) _GetWndAndRectForClick() { // //var w = WndContainer; //with window the mouse functions are more reliable, eg will not click another window // var w = WndTopLevel; //direct parent control may be zero-size etc, eg in Win11 Paint when found with UIA // if (w.Is0) throw new AuException(0, "*get container window"); // RECT r = _GetRect(), rr = r; // print.it(r); // bool retry = false; // gRetry: // bool bad = r.NoArea; // if (!bad) { // bad = (!retry && IsOffscreen) || !r.Intersect(_GetContainerClientRect(w)); // } // print.it(bad); // if (bad) { // if (!retry) { // if (0 == _ScrollTo()) // if (wait.until(-2, () => (r = _GetRect()) != rr)) { // 30.ms(); retry = true; goto gRetry; // } // } // throw new AuException(0, "The UI element rectangle is " + (rr.NoArea ? "empty." : "offscreen.")); // } // return (w, r); // RECT _GetRect() => GetRect(out var r, w) ? r : throw new AuException(0, "*get UI element rectangle"); // //todo: now cannot click in nonclient area. // //never mind: should intersect with all ancestor elements and windows. // // Now no OFFSCREEN state if partially clipped by an ancestor rect. // // Slow and unreliable, because visible children can be not in parent rect. Eg pagetab or treeitem. // //problem: Firefox never updates Rect after ScrollTo. Need to find again. // // Chrome updates after several ms. Chrome does not update OFFSCREEN (or with a delay?). // //problem: some fake container windows may be zero-size etc, and the element is drawn on another window. // // shoulddo: test more. //} /// /// Posts mouse-click messages to the container window, using coordinates in this UI element. /// /// X coordinate in the bounding rectangle of this UI element. Default - center. Examples: 10, ^10 (reverse), .5f (fraction). /// Y coordinate in the bounding rectangle of this UI element. Default - center. /// Can specify the left (default), right or middle button. Also flag for double-click, press or release. /// If not 0, the function calls . If it succeeds, waits scroll number of milliseconds (let the target app update the UI element rectangle etc). Valid values are 0-5000. Tip: if does not scroll, try to find the UI element with flag UIA. /// /// - Failed to get rectangle or container window. /// - The element is invisible/offscreen. /// /// Unsupported button specified. /// /// Does not move the mouse. /// Does not wait until the target application finishes processing the message. /// Works not with all elements. /// Try this function when does not work and you don't want to use MouseClick. /// public void PostClick(Coord x = default, Coord y = default, MButton button = MButton.Left, int scroll = 0) { var (w, r) = _GetWndAndRectForClick(scroll); mouse.PostClick_(w, r, x, y, button); } /// /// Posts mouse-double-click messages to the container window, using coordinates in this UI element. /// /// /// - Failed to get rectangle or container window. /// - The element is invisible/offscreen. /// /// public void PostClickD(Coord x = default, Coord y = default) => PostClick(x, y, MButton.DoubleClick); /// /// Posts mouse-right-click messages to the container window, using coordinates in this UI element. /// /// public void PostClickR(Coord x = default, Coord y = default) => PostClick(x, y, MButton.Right); /// /// Makes this UI element focused () and calls . /// /// Exceptions of and . /// public void SendKeys([ParamString(PSFormat.Keys)] params KKeysEtc[] keysEtc) { bool andSelect = RoleInt is ERole.TREEITEM or ERole.LISTITEM; Focus(andSelect); keys.send(keysEtc); } //rejected: too simple and limited. No x y, scroll, button. ///// ///// Clicks this UI element and calls . ///// ///// If true, calls , else . ///// Exceptions of and . ///// //public void SendKeys(bool doubleClick, [ParamString(PSFormat.keys)] params KKeysEtc[] keysEtc) { // if (doubleClick) MouseClickD(); else MouseClick(); // keys.send(keysEtc); //} //rejected. Use SendKeys, it allows to specify keys to replace (Ctrl+A), append (Ctrl+End), etc. //public void SendText(string text, bool replace, [more options?]) { // Focus(); // ... // keys.sendt(text); //} bool _CheckNeedToggle(bool check) { ThrowIfDisposed_(); var state = State; //if (state.Has(EState.DISABLED)) throw new AuException("Disabled."); //can do more bad than good, eg if DISABLED state is when not actually disabled bool isChecked = state.Has(EState.CHECKED); if (!isChecked) isChecked = state.Has(EState.PRESSED) && !MiscFlags.HasAny(EMiscFlags.UIA | EMiscFlags.Java) && RoleInt == ERole.BUTTON /*&& WndContainer.ClassNameIs("Chrome*")*/; //eg the toggle buttons in Chrome settings have state PRESSED instead of CHECKED return isChecked != check; } /// /// Checks or unchecks this checkbox or toggle-button, or selects this radio button. Uses or . /// /// true to check, false to uncheck. /// Keys for . If "", uses "Space". If null (default), uses . /// Exceptions of or . /// /// Does nothing if the UI element already has the requested checked/unchecked state. Else tries to change the state and does not verify whether it actually worked. /// /// Does not work with 3-state checkboxes and with elements that never have CHECKED state. /// public void Check(bool check = true, [ParamString(PSFormat.Keys)] string keys = null) { if (!_CheckNeedToggle(check)) return; if (keys != null) { SendKeys(keys.Length == 0 ? "Space" : keys); } else { _Invoke(MiscFlags.Has(EMiscFlags.UIA) ? 'c' : 'a'); } } /// /// Checks or unchecks this checkbox or toggle-button, or selects this radio button. To check/uncheck calls callback function. /// /// Callback function that should check or uncheck this UI element. Its parameter is this variable. /// Exceptions of the callback function. /// public void Check(bool check, Action action) { if (!_CheckNeedToggle(check)) return; action(this); } //rejected: Check and Expand overloads for mouse. // Script code like 'e.Check(true, false) looks not good. // Better 'e.Check(true, e => e.MouseClick())' or 'e.Check(true, e => e.PostClick())'. /// /// Expands or collapse this expandable UI element (tree item, combo box, expander, dropdown button). /// /// true to expand, false to collapse. /// If not null, makes this element focused and presses these keys. See . If "", uses keys commonly used for that UI element type, for example Right/Left for TREEITEM, Alt+Down for COMBOBOX. If null, uses or similar functions, which often are available only if the element was found with flag UIA; if unavailable or fails, works like with keys "". /// If not 0, waits for new expanded/collapsed state max this number of seconds; on timeout throws exception, unless negative. /// Ignore initial EXPANDED/COLLAPSED state and always perform the expand/collapse action. Can be useful when EXPANDED/COLLAPSED is incorrect. To ignore final state, use negative waitS instead, for example -0.001. /// Exceptions of . /// The state didn't change in waitS seconds (if > 0). /// /// Does nothing if the UI element already has the requested expanded/collapsed state. /// /// Works with UI elements that have EXPANDED when expanded and COLLAPSED when collapsed. Also with UI elements that have state CHECKED or PRESSED when expanded and don't have this state when collapsed. /// public void Expand(bool expand = true, [ParamString(PSFormat.Keys)] string keys = null, double waitS = 1, bool ignoreState = false) { _Expand(expand, keys, null, waitS, ignoreState); } /// Callback function that should expand or collapse this UI element. Its parameter is this variable. /// Exceptions of the callback function. /// public void Expand(bool expand, Action action, double waitS = 1, bool ignoreState = false) { _Expand(expand, null, action, waitS, ignoreState); } void _Expand(bool expand, string keys, Action action, double waitS, bool ignoreState) { ThrowIfDisposed_(); bool _NeedToggle(bool expand) { var state = State; //print.it(RoleInt, Role, state); //note: ignore DISABLED state. Some non-disabled elements have it. Also probably would need to get state of parent TREEVIEW. bool isExpanded = !state.Has(EState.COLLAPSED) && state.HasAny(EState.EXPANDED | EState.CHECKED | EState.PRESSED); return isExpanded != expand; } if (!ignoreState) if (!_NeedToggle(expand)) return; int how = action != null ? 2 : keys != null ? 1 : 0; if (how == 0) { how = 1; if (MiscFlags.Has(EMiscFlags.UIA)) { if (0 == _InvokeL(expand ? 'E' : 'e')) how = 0; } else if (_ClassicTreeview()) { how = 0; } else { var da = DefaultAction; if (!da.NE()) { if (MiscFlags.Has(EMiscFlags.Java)) { //usually treeitem's default action is "toggleexpand", and it works. Other ways (mouse, focus+keys) are unreliable. //combobox action is "togglePopup", and requires active window. Does not wait until popup closed (good). if (da != "toggleexpand") WndTopLevel.Activate(); if (0 == _InvokeL('a', da)) how = 0; } else { if (0 == _InvokeL()) how = 0; //never mind: not all actions Expand/Collapse, even if TREEITEM. Eg in Thunderbird. // Could use keys etc if state didn't change eg in 0.5 s, but it can make less reliable. // Let users choose another overload. } } else if (RoleInt == ERole.COMBOBOX) { //classic combobox? if (0 == _GetWnd(out wnd w) && w.CommonControlType == WControlType.Combobox) { w.SendNotify(0x014F, expand ? 1 : 0); //CB_SHOWDROPDOWN how = 0; } } } } if (how == 1) { Focus(andSelect: RoleInt is ERole.TREEITEM or ERole.LISTITEM or ERole.Custom); //exception if fails if (keys.NE()) { var role = MiscFlags.Has(EMiscFlags.Java) && Role == "combo box" ? ERole.COMBOBOX : RoleInt; keys = role switch { ERole.TREEITEM or ERole.LISTITEM or ERole.Custom => expand ? "Right" : "Left", //LISTITEM for treeviews made from listviews (not tested); Custom because we prefer treeviews ERole.COMBOBOX or ERole.DROPLIST => expand ? "Alt+Down" : "Esc", //DROPLIST used by the classic date/time picker, but does not work because state always 0 ERole.BUTTON or ERole.CHECKBOX => "Space", //eg expander ERole.BUTTONDROPDOWN or ERole.BUTTONDROPDOWNGRID or ERole.BUTTONMENU => expand ? "Space" : "Esc", _ => expand ? "Down" : "Esc", //eg classic toolbar's SPLITBUTTON's dropdown part (MENUITEM); also classic SPLITBUTTON }; } Au.keys.send(keys); } else if (how == 2) { action(this); } if (waitS != 0) { //10.ms(); wait.until(waitS, () => !_NeedToggle(expand)); } bool _ClassicTreeview() { if (Item == 0 || RoleInt != ERole.TREEITEM) return false; if (0 != _GetWnd(out wnd w) || w.CommonControlType != WControlType.Treeview) return false; if (osVersion.is32BitProcessAnd64BitOS && !w.Is32Bit) return false; //cannot get 64-bit HTREEITEM nint hi = w.Send(TVM_MAPACCIDTOHTREEITEM, Item); if (hi == 0) return false; _Expand_ClassicTreeview(w, hi, expand); return true; } } static void _Expand_ClassicTreeview(wnd w, nint hi, bool expand) { #if false //like MSAA Invoke. Bad: no TVN_ITEMEXPANDED etc; eg does not change folder icon open/closed. w.Send(0x1102, expand ? 2 : 1, hi); //TVM_EXPAND(TVE_EXPAND:TVE_COLLAPSE) if (expand) w.Send(0x1114, 0, hi); //TVM_ENSUREVISIBLE #else //like UIA expand/collapse. Bad: dirty; selects, which may do more than expand/collapse; for TVSI_NOSINGLEEXPAND need manifest. for (int i = 0; i < 5; i++) { //eg in old VS first time returns 0, although works. UIA sends 2 times. if (0 != w.Send(TVM_SELECTITEM, TVGN_CARET | TVSI_NOSINGLEEXPAND, hi)) break; Debug_.PrintIf(i > 0, "elm.Expand"); //if (i > 0) wait.ms(i * 10); } int k = (int)(expand ? KKey.Right : KKey.Left); w.SendNotify(Api.WM_KEYDOWN, k); w.SendNotify(Api.WM_KEYUP, k); 10.ms(); //UIA posts, but then eg in old VS does not work if the control wasn't focused. Maybe that is why UIA always sets real focus. //CONSIDER: wnd.PostKey(), wnd.PostText(). #endif } /// /// Expands multiple treeview control items using a path string. /// /// /// String or array consisting of names () of TREEITEM elements, like "One|Two|Three" or ["One", "Two", "Three"]. /// Name string format: [wildcard expression](xref:wildcard_expression). /// /// null or keys to use to expand each element specified in path. See . /// If not 0, after expanding each element waits for expanded state max this number of seconds; on timeout throws exception, unless negative. Also waits for each element this number of seconds; always exception if not found. /// Find but don't expand the last element specified in path. For example if it's not a folder, and therefore can't expand it, but you need to find it (this function returns it). /// of the last element specified in path. /// path contains an invalid wildcard expression ("**options " or regular expression). /// Failed to find an element specified in path. /// Failed. /// The state didn't change in waitS seconds (if > 0). /// The treeview control type is not supported when this is a 32-bit process running on 64-bit OS (unlikely). /// Exceptions of . /// /// This element can be a TREE or TREEITEM. If it is a collapsed TREEITEM, expands it. Then finds and expands elements specified in path. /// /// Does not work if all TREEITEM elements in the TREE control are its direct children, unless it's the standard Windows treeview control. /// public elm Expand(Strings path, [ParamString(PSFormat.Keys)] string keys = null, double waitS = 3, bool notLast = false) { return _ExpandPath(path, keys, null, waitS, notLast); } /// Callback function that should expand UI elements. /// Exceptions of the callback function. /// /// public elm Expand(Strings path, Action action, double waitS = 3, bool notLast = false) { return _ExpandPath(path, null, action, waitS, notLast); } elm _ExpandPath(Strings path, string keys, Action action, double waitS, bool notLast) { ThrowIfDisposed_(); var a = path.ToArray(); var e = this; //for classic treeview controls need special code if not UIA, because the MSAA tree is flat if (!MiscFlags.HasAny(EMiscFlags.UIA | EMiscFlags.Java) && 0 == _GetWnd(out wnd w) && w.CommonControlType == WControlType.Treeview) { if (osVersion.is32BitProcessAnd64BitOS && !w.Is32Bit) throw new NotSupportedException("32-bit process."); //cannot get 64-bit HTREEITEM nint hi = 0; if (Item > 0) { hi = w.Send(TVM_MAPACCIDTOHTREEITEM, Item); if (hi == 0) throw new AuException(); _ExpandIfNeed(this, hi); } for (int i = 0; i < a.Length; i++) { var name = a[i]; bool ok = waitS == 0 ? _Find(name) : wait.until(-Math.Abs(waitS), () => _Find(name)); if (!ok) throw new NotFoundException("Not found elm expand path part: " + name); if (notLast && i == a.Length - 1) break; _ExpandIfNeed(e, hi); } bool _Find(wildex name) { elm temp = null; for (nint h = w.Send(TVM_GETNEXTITEM, hi == 0 ? TVGN_ROOT : TVGN_CHILD, hi); h != 0; h = w.Send(TVM_GETNEXTITEM, TVGN_NEXT, h)) { int item = (int)w.Send(TVM_MAPHTREEITEMTOACCID, h); if (item == 0) throw new AuException(); var acc = new Cpp.Cpp_Acc(_iacc, item) { misc = _misc }; if (Item == 0) { acc.misc.level++; acc.misc.roleByte = (byte)ERole.TREEITEM; } var s = _GetStringPropL(acc, 'n'); //the slowest part of this 'for'. FUTURE: if somewhere too slow, could make inproc. GC.KeepAlive(this); if (name.Match(s)) { if (temp == null) temp = new(acc, addRef: true); else temp.Item = item; e = temp; hi = h; return true; } } return false; } void _ExpandIfNeed(elm e, nint hi) { if (_IsExpanded(w, hi)) return; if (keys != null) e.SendKeys(keys.Length > 0 ? keys : "Right"); else if (action != null) action(e); else _Expand_ClassicTreeview(w, hi, true); //p1.Next(); if (waitS != 0) wait.until(waitS, () => _IsExpanded(w, hi)); else _IsExpanded(w, hi); //usually waits } static bool _IsExpanded(wnd w, nint hi) => 0 != ((int)w.Send(TVM_GETITEMSTATE, hi, TVIS_EXPANDED) & TVIS_EXPANDED); } else { if (State.Has(EState.COLLAPSED)) _Expand(true, keys, action, waitS, ignoreState: true); foreach (var name in a) { int level = e.Level; e = e.Elm[null, name, "level=0", also: o => o.State.HasAny(EState.EXPANDED | EState.COLLAPSED)].Find(-(Math.Abs(waitS) + .01)); if (e == null) throw new NotFoundException("Not found elm expand path part: " + name); e.Level = level + 1; e._Expand(true, keys, action, waitS, ignoreState: false); } } return e; } /// /// Finds and selects an item in the drop-down list of this combo box or drop-down button. /// /// /// Item name (). /// String format: [wildcard expression](xref:wildcard_expression). /// /// /// Try this parameter if the function fails to select the item etc. /// /// /// In the string can be used these characters to specify how to select the item and close the drop-down list: ///
i - call . ///
s - call . ///
c - close the list with . ///
m - call ; often can't be used because fails to get correct rectangle or to scroll. ///
k - call and (Home, Down, Enter). ///
• space - nothing. ///
/// /// If the string is null (default) or "" or does not contain these characters, the function tries to detect and use what usually works for this UI element type, but it's impossible to detect always. /// /// Usually need just a single character (string like "i" or "m"). If there are more characters, the functions are called in the specified order. /// /// The string also can contain sleep times. For example "300m" will wait 300 ms and click; the first sleep will be between expanding and finding. /// /// If the string starts with "~", the function does not expand the drop-down list (it should be already expanded). /// /// If the string isn't null/empty but does not contain characters iscmk (for example is " " or "~ " or "200 "), the function does not select/close. For example these codes do the same: e.ComboSelect("Red", "i"); and e.ComboSelect("Red", " ").Invoke();. /// /// Seconds to wait for expanded state (if not 0) and for the item. Can be negative to avoid timeout exceptions. /// The item. /// Error in item string (wildex options or regex) or how. /// Item not found. /// Exceptions of used functions. /// /// The function at first calls to show the drop-down list, unless how starts with "~". Then finds the item by name, selects it and closes the drop-down list. /// public elm ComboSelect(string item, string how = null, double waitS = 3) { //Works with most tested combobox types: native, web, WPF, UWP, ribbon, Office, Qt, Java, JavaFX. // Does not work with OpenOffice: can't expand, need Invoke() but it isn't called because there is no DefaultAction. Didn't try to select items. Never mind, anyway no API in dialogs, only in main window. // Not tested GTK. Don't have apps with working API. //Problems in Chrome: // Select() does not work. Use Invoke(); it also collapses. //Problems in Firefox: // Standard CB in FF does not change state e.Select(); Expand(!true, waitS: -0.01, ignoreState: ignoreState); } #endregion return e; void _Keys() { var a = e.Parent.Elm[e.Role, prop: "level=0", flags: EFFlags.HiddenToo].FindAll(); //these are slow, but much faster than keys.send wildex wild = item; int i = Array.FindIndex(a, o => wild.Match(o.Name)); if (i < 0) throw new AuException(); Au.keys.send($"Home PgUp*{a.Length / 4} Down*{i} Enter"); //Home doesn't work with editable CB; PgUp may not work with some CB } void _Sleep() { if (!how[h].IsAsciiDigit()) { if (e == null) return; throw new ArgumentException("Invalid character " + how[h], "how"); } if (!how.ToInt(out int ms, h, out h)) throw new ArgumentException("Invalid number", "how"); ms.ms(); } void _FindWebNotCOMBOBOX() { //Web pages have multiple CB types. Sometimes the dropdown list isn't a descendant of the CB. // In Google Advanced Search it is LIST with 2 children: MENUITEM and LISTITEM with same name (the selected). // The list is a MENUPOPUP/MENUITEM near the bottom of the document. It may not contain the selected item. // In Github profile Stars it is BUTTON with 0 children. // The list is a MENUPOPUP/MENUITEM in its parent. elm pa = null, doc = null; var loop = new WaitLoop(waitS); for (; ; ) { e = f.Find(); //if the item is already selected, the dropdown list may not contain it, and this finds it if (e != null) break; pa ??= Parent; if (pa == null) break; var mp = pa.Elm["MENUPOPUP"].Find(); if (mp != null) { e = mp.Elm["MENUITEM", item].Find(); } else { if (doc == null) { for (var p = pa; p != null; p = p.Parent) if (p.RoleInt == ERole.DOCUMENT) { doc = p; break; } } if (doc != null) { e = doc.Elm["MENUPOPUP", flags: EFFlags.Reverse].Find()?.Elm["MENUITEM", item].Find(); } } if (e != null) { invoke = true; //selects and closes in FF too; else difficult/unreliable. break; } if (!loop.Sleep()) break; } } } //wnd _GetOwnedPopupWindow(wnd w, RECT? r = null) { // var p = w.Window.Get.EnabledOwned(); // if (!p.Is0) { // var k = r ?? Rect; // int i = k.Height / 2; // k.Inflate(i, i); // var pr = p.Rect; // if (!pr.IntersectsWith(k) || pr.Height < k.Height * 2) p = default; // } // return p; //} const int TVM_MAPACCIDTOHTREEITEM = 0x112A; const int TVM_GETNEXTITEM = 0x110A; const int TVM_MAPHTREEITEMTOACCID = 0x112B; const int TVM_GETITEMSTATE = 0x1127; const int TVM_ENSUREVISIBLE = 0x1114; const int TVM_SELECTITEM = 0x110B; const int TVGN_ROOT = 0x0; const int TVGN_CHILD = 0x4; const int TVGN_NEXT = 0x1; const int TVGN_CARET = 0x9; const int TVSI_NOSINGLEEXPAND = 0x8000; const int TVIS_EXPANDED = 0x20; const int LVM_ENSUREVISIBLE = 0x1013; const int LB_SETTOPINDEX = 0x197; } } //rejected: // MenuSelect(path) - click, wait for popup menu, find/click, wait for another popup, .... Can check/uncheck. // Much easier and better to use keys. // ContextMenuSelect(path). // Let use MouseClickR and keys. //TEST: // IUIAutomationScrollPattern: Scroll, SetScrollPercent. // IUIAutomationWindowPattern: Close, WaitForInputIdle, CurrentIsModal, CurrentWindowInteractionState. ================================================ FILE: Au/UI objects/elm_types.cs ================================================ namespace Au.Types { /// /// Flags for "find UI element" functions (). /// [Flags] public enum EFFlags { /// /// Search in reverse order. It can make faster. /// When control class or id is specified in the prop argument, controls are searched not in reverse order. Only UI elements in them are searched in reverse order. /// Reverse = 1, /// /// The UI element can be invisible. /// Without this flag skips UI elements that are invisible (have state INVISIBLE) or are descendants of invisible WINDOW, DOCUMENT, PROPERTYPAGE, GROUPING, ALERT, MENUPOPUP. /// Regardless of this flag, always skips invisible standard UI elements of nonclient area: TITLEBAR, MENUBAR, SCROLLBAR, GRIP. /// HiddenToo = 2, /// /// Always search in MENUITEM. /// Without this flag skips MENUITEM descendant elements (for speed), unless role argument is MENUITEM or MENUPOPUP or searching in web page. /// MenuToo = 4, /// /// Search only in the client area of the window or control. /// Skips the title bar, standard menubars and scrollbars. Searches only in the client area root UI element (but will not find the UI element itself). /// When control class or id is specified in the prop argument, this flag is applied to these controls. Not applied to other controls. /// Don't use this flag when searching in elm or web page (role prefix "web:" etc) or with flag UIA. /// ClientArea = 8, /// /// Search without loading dll into the target process. /// Disadvantages: 1. Much slower. 2. Some properties are not supported, for example HTML attributes (while searching and later). 3. And more. /// Even without this flag, the default search method is not used with Windows Store app windows, console windows, most Java windows, windows of protected processes and processes of higher [](xref:uac) integrity level. /// Some windows have child controls that belong to a different process or thread than the window. For example the Windows Task Scheduler window. When searching in such windows, the default search method is not used when searching in these controls. Workaround - find the control ( etc) and search in it. /// Don't need this flag when searching in elm (then it is inherited from the elm variable). /// See also: . /// NotInProc = 0x100, /// /// Use UI Automation API. /// Need this flag to find UI elements in windows that don't support accessible objects but support UI Automation elements. /// UI elements found with this flag never have HtmlX properties, but can have UiaX properties. /// This flag can be used with most other windows too. /// Don't use this flag when searching in elm (then it is inherited from the elm variable) or web page (role prefix "web:" etc). /// See also: . /// UIA = 0x200, //Internal. See Enum_.AFFlags_Mark. //Mark = 0x10000, } /// /// Adds internal members to public enums. /// internal static partial class Enum_ { /// /// Used by Delm, together with ElmMiscFlags_Marked. /// internal static EFFlags EFFlags_Mark = (EFFlags)0x10000; /// /// Used by Delm, together with AFFlags_Mark. /// internal static EMiscFlags EMiscFlags_Marked = (EMiscFlags)128; internal static EXYFlags EXYFlags_DpiScaled = (EXYFlags)0x10000; internal static EXYFlags EXYFlags_Fail = (EXYFlags)0x20000; //currently not used } /// /// Flags for . /// [Flags] public enum EWFlags { /// Don't throw exception when fails. Then returns null. NoThrow = 1, /// /// Don't load dll into the target process. /// More info: . /// NotInProc = 2, } /// /// Flags for . /// [Flags] public enum EXYFlags { /// /// Don't load dll into the target process. /// More info: . /// NotInProc = 1, /// /// Use UI Automation API. /// More info: . /// UIA = 2, /// /// Get the direct parent UI element if probably it would be much more useful, for example if its role is LINK or BUTTON. /// Usually links have one or more children of type TEXT, STATICTEXT, IMAGE or other. /// PreferLink = 4, [Obsolete] #pragma warning disable CS1591 //Missing XML comment for publicly visible type or member TrySmaller = 8, #pragma warning restore CS1591 //Missing XML comment for publicly visible type or member /// /// Use UI Automation API if the default API fails and in some other cases. /// The Find UI element tool uses this flag when UIA is in indeterminate state. /// OrUIA = 16, //note: don't change values. They are passed to the cpp function. } /// /// Flags for . /// [Flags] public enum EFocusedFlags { /// /// Don't load dll into the target process. /// More info: . /// NotInProc = 1, /// /// Use UI Automation API. /// Need this flag with some windows that don't support accessible objects but support UI Automation elements. Can be used with most other windows too. /// More info: . /// UIA = 2, //note: don't change values. They are passed to the cpp function. } /// /// Flags returned by . /// [Flags] public enum EMiscFlags : byte { /// /// This UI element was retrieved by the dll loaded into its process. /// More info: . /// InProc = 1, /// /// This UI element was retrieved using UI Automation API. /// More info: . /// UIA = 2, /// /// This UI element was retrieved using Java Access Bridge API. /// More info: . /// Java = 4, //Internal. See Enum_.ElmMiscFlags_Marked. //Marked = 128, } #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member /// /// Object ids of window parts and some special UI elements. /// Used with /// /// /// Most names are as in API AccessibleObjectFromWindow documentation but without prefix OBJID_. /// public enum EObjid { WINDOW = 0, SYSMENU = -1, TITLEBAR = -2, MENU = -3, CLIENT = -4, VSCROLL = -5, HSCROLL = -6, SIZEGRIP = -7, CARET = -8, CURSOR = -9, ALERT = -10, SOUND = -11, /// Not used with . QUERYCLASSNAMEIDX = -12, /// Not used with . NATIVEOM = -16, /// Not used with . UiaRootObjectId = -25, //ours /// /// The root Java UI element. Can be used when the window's class name starts with "SunAwt". /// Java = -100, /// /// Use UI Automation API. /// More info: . /// UIA = -101, } /// /// Standard roles of UI elements. /// Used with /// /// /// Most names are as in API IAccessible.get_accRole Object Roles documentation but without prefix ROLE_SYSTEM_. These are renamed: PUSHBUTTON to BUTTON, CHECKBUTTON to CHECKBOX, GRAPHIC to IMAGE, OUTLINE to TREE, OUTLINEITEM to TREEITEM, OUTLINEBUTTON to TREEBUTTON. /// public enum ERole { TITLEBAR = 0x1, MENUBAR = 0x2, SCROLLBAR = 0x3, GRIP = 0x4, SOUND = 0x5, CURSOR = 0x6, CARET = 0x7, ALERT = 0x8, WINDOW = 0x9, CLIENT = 0xA, MENUPOPUP = 0xB, MENUITEM = 0xC, TOOLTIP = 0xD, APPLICATION = 0xE, DOCUMENT = 0xF, PANE = 0x10, CHART = 0x11, DIALOG = 0x12, BORDER = 0x13, GROUPING = 0x14, SEPARATOR = 0x15, TOOLBAR = 0x16, STATUSBAR = 0x17, TABLE = 0x18, COLUMNHEADER = 0x19, ROWHEADER = 0x1A, COLUMN = 0x1B, ROW = 0x1C, CELL = 0x1D, LINK = 0x1E, HELPBALLOON = 0x1F, CHARACTER = 0x20, LIST = 0x21, LISTITEM = 0x22, TREE = 0x23, //OUTLINE TREEITEM = 0x24, //OUTLINEITEM PAGETAB = 0x25, PROPERTYPAGE = 0x26, INDICATOR = 0x27, IMAGE = 0x28, //GRAPHIC STATICTEXT = 0x29, TEXT = 0x2A, BUTTON = 0x2B, //PUSHBUTTON CHECKBOX = 0x2C, //CHECKBUTTON RADIOBUTTON = 0x2D, COMBOBOX = 0x2E, DROPLIST = 0x2F, PROGRESSBAR = 0x30, DIAL = 0x31, HOTKEYFIELD = 0x32, SLIDER = 0x33, SPINBUTTON = 0x34, DIAGRAM = 0x35, ANIMATION = 0x36, EQUATION = 0x37, BUTTONDROPDOWN = 0x38, BUTTONMENU = 0x39, BUTTONDROPDOWNGRID = 0x3A, WHITESPACE = 0x3B, PAGETABLIST = 0x3C, CLOCK = 0x3D, SPLITBUTTON = 0x3E, IPADDRESS = 0x3F, TREEBUTTON = 0x40, //OUTLINEBUTTON /// Failed to get role. None = 0, /// Not one of predefined roles. Usually string. Custom = 0xFF, } /// /// UI element state flags. /// Used by . /// /// /// Most names are as in API IAccessible.get_accState Object State Constants documentation but without prefix STATE_SYSTEM_. /// [Flags] public enum EState { //NORMAL = 0x0, DISABLED = 0x1, //original name UNAVAILABLE SELECTED = 0x2, FOCUSED = 0x4, PRESSED = 0x8, CHECKED = 0x10, MIXED = 0x20, READONLY = 0x40, HOTTRACKED = 0x80, DEFAULT = 0x100, EXPANDED = 0x200, COLLAPSED = 0x400, BUSY = 0x800, FLOATING = 0x1000, MARQUEED = 0x2000, ANIMATED = 0x4000, INVISIBLE = 0x8000, OFFSCREEN = 0x10000, SIZEABLE = 0x20000, MOVEABLE = 0x40000, SELFVOICING = 0x80000, FOCUSABLE = 0x100000, SELECTABLE = 0x200000, LINKED = 0x400000, TRAVERSED = 0x800000, MULTISELECTABLE = 0x1000000, EXTSELECTABLE = 0x2000000, ALERT_LOW = 0x4000000, ALERT_MEDIUM = 0x8000000, ALERT_HIGH = 0x10000000, PROTECTED = 0x20000000, HASPOPUP = 0x40000000, } /// /// UI element selection flags. /// Used by . /// /// /// The names are as in API IAccessible.accSelect documentation but without prefix SELFLAG_. /// [Flags] public enum ESelect { TAKEFOCUS = 0x1, TAKESELECTION = 0x2, EXTENDSELECTION = 0x4, ADDSELECTION = 0x8, REMOVESELECTION = 0x10, } /// /// Event constants for . /// /// /// The names are as in API SetWinEventHook Event Constants documentation but without prefix EVENT_. /// public enum EEvent { MIN = 0x1, MAX = 0x7FFFFFFF, SYSTEM_SOUND = 0x1, SYSTEM_ALERT = 0x2, SYSTEM_FOREGROUND = 0x3, SYSTEM_MENUSTART = 0x4, SYSTEM_MENUEND = 0x5, SYSTEM_MENUPOPUPSTART = 0x6, SYSTEM_MENUPOPUPEND = 0x7, SYSTEM_CAPTURESTART = 0x8, SYSTEM_CAPTUREEND = 0x9, SYSTEM_MOVESIZESTART = 0xA, SYSTEM_MOVESIZEEND = 0xB, SYSTEM_CONTEXTHELPSTART = 0xC, SYSTEM_CONTEXTHELPEND = 0xD, SYSTEM_DRAGDROPSTART = 0xE, SYSTEM_DRAGDROPEND = 0xF, SYSTEM_DIALOGSTART = 0x10, SYSTEM_DIALOGEND = 0x11, SYSTEM_SCROLLINGSTART = 0x12, SYSTEM_SCROLLINGEND = 0x13, SYSTEM_SWITCHSTART = 0x14, SYSTEM_SWITCHEND = 0x15, SYSTEM_MINIMIZESTART = 0x16, SYSTEM_MINIMIZEEND = 0x17, SYSTEM_DESKTOPSWITCH = 0x20, SYSTEM_SWITCHER_APPGRABBED = 0x24, SYSTEM_SWITCHER_APPOVERTARGET = 0x25, SYSTEM_SWITCHER_APPDROPPED = 0x26, SYSTEM_SWITCHER_CANCELLED = 0x27, SYSTEM_IME_KEY_NOTIFICATION = 0x29, SYSTEM_END = 0xFF, OEM_DEFINED_START = 0x101, OEM_DEFINED_END = 0x1FF, UIA_EVENTID_START = 0x4E00, UIA_EVENTID_END = 0x4EFF, UIA_PROPID_START = 0x7500, UIA_PROPID_END = 0x75FF, CONSOLE_CARET = 0x4001, CONSOLE_UPDATE_REGION = 0x4002, CONSOLE_UPDATE_SIMPLE = 0x4003, CONSOLE_UPDATE_SCROLL = 0x4004, CONSOLE_LAYOUT = 0x4005, CONSOLE_START_APPLICATION = 0x4006, CONSOLE_END_APPLICATION = 0x4007, CONSOLE_END = 0x40FF, OBJECT_CREATE = 0x8000, OBJECT_DESTROY = 0x8001, OBJECT_SHOW = 0x8002, OBJECT_HIDE = 0x8003, OBJECT_REORDER = 0x8004, OBJECT_FOCUS = 0x8005, OBJECT_SELECTION = 0x8006, OBJECT_SELECTIONADD = 0x8007, OBJECT_SELECTIONREMOVE = 0x8008, OBJECT_SELECTIONWITHIN = 0x8009, OBJECT_STATECHANGE = 0x800A, OBJECT_LOCATIONCHANGE = 0x800B, OBJECT_NAMECHANGE = 0x800C, OBJECT_DESCRIPTIONCHANGE = 0x800D, OBJECT_VALUECHANGE = 0x800E, OBJECT_PARENTCHANGE = 0x800F, OBJECT_HELPCHANGE = 0x8010, OBJECT_DEFACTIONCHANGE = 0x8011, OBJECT_ACCELERATORCHANGE = 0x8012, OBJECT_INVOKED = 0x8013, OBJECT_TEXTSELECTIONCHANGED = 0x8014, OBJECT_CONTENTSCROLLED = 0x8015, SYSTEM_ARRANGMENTPREVIEW = 0x8016, OBJECT_CLOAKED = 0x8017, OBJECT_UNCLOAKED = 0x8018, OBJECT_LIVEREGIONCHANGED = 0x8019, OBJECT_HOSTEDOBJECTSINVALIDATED = 0x8020, OBJECT_DRAGSTART = 0x8021, OBJECT_DRAGCANCEL = 0x8022, OBJECT_DRAGCOMPLETE = 0x8023, OBJECT_DRAGENTER = 0x8024, OBJECT_DRAGLEAVE = 0x8025, OBJECT_DRAGDROPPED = 0x8026, OBJECT_IME_SHOW = 0x8027, OBJECT_IME_HIDE = 0x8028, OBJECT_IME_CHANGE = 0x8029, OBJECT_TEXTEDIT_CONVERSIONTARGETCHANGED = 0x8030, OBJECT_END = 0x80FF, AIA_START = 0xA000, AIA_END = 0xAFFF, IA2_ACTION_CHANGED = 0x101, IA2_ACTIVE_DESCENDANT_CHANGED = 0x102, IA2_DOCUMENT_ATTRIBUTE_CHANGED = 0x103, IA2_DOCUMENT_CONTENT_CHANGED = 0x104, IA2_DOCUMENT_LOAD_COMPLETE = 0x105, IA2_DOCUMENT_LOAD_STOPPED = 0x106, IA2_DOCUMENT_RELOAD = 0x107, IA2_HYPERLINK_END_INDEX_CHANGED = 0x108, IA2_HYPERLINK_NUMBER_OF_ANCHORS_CHANGED = 0x109, IA2_HYPERLINK_SELECTED_LINK_CHANGED = 0x10a, IA2_HYPERTEXT_LINK_ACTIVATED = 0x10b, IA2_HYPERTEXT_LINK_SELECTED = 0x10c, IA2_HYPERLINK_START_INDEX_CHANGED = 0x10d, IA2_HYPERTEXT_CHANGED = 0x10e, IA2_HYPERTEXT_NLINKS_CHANGED = 0x10f, IA2_OBJECT_ATTRIBUTE_CHANGED = 0x110, IA2_PAGE_CHANGED = 0x111, IA2_SECTION_CHANGED = 0x112, IA2_TABLE_CAPTION_CHANGED = 0x113, IA2_TABLE_COLUMN_DESCRIPTION_CHANGED = 0x114, IA2_TABLE_COLUMN_HEADER_CHANGED = 0x115, IA2_TABLE_MODEL_CHANGED = 0x116, IA2_TABLE_ROW_DESCRIPTION_CHANGED = 0x117, IA2_TABLE_ROW_HEADER_CHANGED = 0x118, IA2_TABLE_SUMMARY_CHANGED = 0x119, IA2_TEXT_ATTRIBUTE_CHANGED = 0x11a, IA2_TEXT_CARET_MOVED = 0x11b, IA2_TEXT_CHANGED = 0x11c, IA2_TEXT_COLUMN_CHANGED = 0x11d, IA2_TEXT_INSERTED = 0x11e, IA2_TEXT_REMOVED = 0x11f, IA2_TEXT_UPDATED = 0x120, IA2_TEXT_SELECTION_CHANGED = 0x121, IA2_VISIBLE_DATA_CHANGED = 0x122, } /// /// Flags for . /// /// /// The names are as in API SetWinEventHook documentation but without prefix WINEVENT_. /// There are no flags for OUTOFCONTEXT and INCONTEXT. OUTOFCONTEXT is default (0). INCONTEXT cannot be used in managed code. /// [Flags] public enum EHookFlags { None, /// Don't receive events generated by this thread. SKIPOWNTHREAD = 0x1, /// Don't receive events generated by threads of this process. SKIPOWNPROCESS = 0x2, //OUTOFCONTEXT = 0x0, //INCONTEXT = 0x4, } /// /// Used with . /// public class EProperties { public string Role, Name, Value, Description, Help, DefaultAction, KeyboardShortcut, UiaId, UiaCN, OuterHtml, InnerHtml; public EState State; public RECT Rect; public wnd WndContainer; public Dictionary HtmlAttributes; } #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member } ================================================ FILE: Au/UI objects/ocr.cs ================================================ using System.Drawing; namespace Au; /// /// OCR functions. Recognizes text on screen or in image, gets word positions, finds text, clicks found words. /// public class ocr { readonly IFArea _area; static regexp s_rx1 = new(@"\s+"); internal ocr(OcrWord[] words, IFArea area) { _area = area; Words = words; TextForFind = _BuildText(true); } /// /// Recognized words (text, rectangle, etc). /// public OcrWord[] Words { get; private set; } /// /// Recognized text as single line. /// Any whitespace between words is replaced with single space. /// and similar functions search in this text. /// public string TextForFind { get; private set; } /// /// Recognized text. /// public string Text => _textML ??= _BuildText(false); string _textML; string _BuildText(bool forFind) { var b = new StringBuilder(); foreach (var word in Words) { var s = word.Separator; if (forFind) if (!s.NE() && s != " ") s = s is "\r\n" or "\n" ? " " : s_rx1.Replace(s, " "); //replace any whitespace with " " b.Append(s); if (forFind) word.Offset = b.Length; b.Append(word.Text); } return b.ToString(); } /// /// Index (in ) of the first found word. /// If did not search for text (for example called ), this property is 0. /// public int FoundWordIndex { get; internal set; } /// /// Range of found text in . /// public StartEnd FoundTextRange { get; internal set; } //internal void SortResults_() { // Array.Sort(Words, (x, y) => { // if (y.Rect.top >= x.Rect.bottom) return -1; // if (x.Rect.top >= y.Rect.bottom) return 1; // return x.Rect.left - y.Rect.left; // }); //} internal void AdjustResults_(POINT resultOffset, OcrFlags flags) { if (flags.HasAny(OcrFlags.WindowDC | OcrFlags.PrintWindow) && Dpi.GetScalingInfo_(_area.W, out bool scaled, out _, out _) && scaled) { int d1 = screen.of(_area.W.Window).Dpi, d2 = Dpi.OfWindow(_area.W); foreach (ref var word in Words.AsSpan()) { var r = word.Rect; r.left = Math2.MulDiv(r.left, d1, d2); r.top = Math2.MulDiv(r.top, d1, d2); r.right = Math2.MulDiv(r.right, d1, d2); r.bottom = Math2.MulDiv(r.bottom, d1, d2); word.Rect = r; } } if (resultOffset.x != 0 || resultOffset.y != 0) { foreach (ref var word in Words.AsSpan()) { var r = word.Rect; r.Offset(resultOffset.x, resultOffset.y); word.Rect = r; } } } /// /// Gets or sets the default OCR engine. /// /// /// If not set, the get function returns a static object. To use another OCR engine, create and assign an object of type , , , or other class that implements . /// public static IOcrEngine engine { get => s_engine ??= new OcrWin10(); set { s_engine = value; } } static IOcrEngine s_engine; /// /// Performs OCR (text recognition). /// /// /// Returns an object that contains recognized words etc. /// Returns null if the area is empty. /// /// /// /// Captures image from screen or window (unless area is ) and passes it to the OCR engine (calls ). Then creates and returns an object that contains results. /// /// The speed depends on engine, area size and amount of text. /// public static ocr recognize(IFArea area, OcrFlags flags = 0, double scale = 0, IOcrEngine engine = null) { engine ??= ocr.engine; area.Before_(flags.HasAny(OcrFlags.WindowDC | OcrFlags.PrintWindow)); if (!area.GetOcrData_(flags, out var b, out var resultOffset)) return null; scale = area.GetOcrScale_(scale, engine); var a = engine.Recognize(b, dispose: area.Type != IFArea.AreaType.Bitmap, scale); var r = new ocr(a, area); r.AdjustResults_(resultOffset, flags); return r; } /// /// Performs OCR (text recognition) and finds text in results. /// /// /// Returns an object that contains the word index and can click it etc. /// Returns null if not found. /// /// /// On-screen area or image: ///
- window or control (its client area). ///
- UI element. ///
- image. ///
- a rectangle area in screen. ///
- can contain , or . Also allows to specify a rectangle in it, which makes the area smaller and the function faster. Example: new(w, (left, top, width, height)). /// /// /// Text to find in . Can have prefix: ///
"**r " - PCRE regular expression. Example: @"**r \bwhole words\b". ///
"**R " - .NET regular expression. ///
"**i " - case-insensitive. ///
"**t " - case-sensitive (default). /// /// /// /// Scale factor (how much to resize the image before performing OCR). /// Value 2 or 3 may improve results of OCR engine or . /// If 0 (default), depends on engine's and area's DPI. /// /// OCR engine. Default: ( if not specified). /// Skip this count of found text instances. /// Invalid window handle (the area argument). /// An argument is/contains a null/invalid value. /// Something failed. /// /// The function captures image from screen or window (unless area is ) and passes it to the OCR engine (calls ). Then finds the specified text in results. If found, creates and returns an object that contains results. /// /// The speed depends on engine, area size and amount of text. /// public static ocr find(IFArea area, string text, OcrFlags flags = 0, double scale = 0, IOcrEngine engine = null, int skip = 0) => new ocrFinder(text, flags, scale, engine, skip).Find(area); /// /// Performs OCR (text recognition) and finds text in results. Can wait and throw . /// /// /// Returns an object that contains the word index and can click it etc. /// If not found, throws exception or returns null (if wait negative). /// /// The wait timeout, seconds. If 0, does not wait. If negative, does not throw exception when not found. /// /// Invalid window handle (the area argument), or the window closed while waiting. /// public static ocr find(Seconds wait, IFArea area, string text, OcrFlags flags = 0, double scale = 0, IOcrEngine engine = null, int skip = 0) => new ocrFinder(text, flags, scale, engine, skip).Find(area, wait); /// /// Performs OCR (text recognition) and finds text in results. Waits until found. /// /// Returns an object that contains the word index and can click it etc. On timeout returns null if timeout is negative; else exception. /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// timeout time has expired (if > 0). /// Invalid window handle (the area argument), or the window closed while waiting. /// public static ocr wait(Seconds timeout, IFArea area, string text, OcrFlags flags = 0, double scale = 0, IOcrEngine engine = null, int skip = 0) => new ocrFinder(text, flags, scale, engine, skip).Wait(timeout, area); /// /// Performs OCR (text recognition) and waits until the specified text does not exist in results. /// /// Returns true. On timeout returns false if timeout is negative; else exception. /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// public static bool waitNot(Seconds timeout, IFArea area, string text, OcrFlags flags = 0, double scale = 0, IOcrEngine engine = null, int skip = 0) => new ocrFinder(text, flags, scale, engine, skip).WaitNot(timeout, area); /// /// Gets the rectangle of the found word. /// /// Convert to screen coordinates. If false, it's in area coordinates (window client area, etc) without rectangle offset. /// Word index offset from . public RECT GetRect(bool inScreen, int word = 0) { int i = FoundWordIndex + word; if ((uint)i >= Words.Length) throw new ArgumentOutOfRangeException("word"); var r = Words[i].Rect; if (inScreen) { if (_area.Type == IFArea.AreaType.Wnd) { _area.W.MapClientToScreen(ref r); } else if (_area.Type == IFArea.AreaType.Elm) { if (!_area.E.GetRect(out var rr)) return default; r.Offset(rr.left, rr.top); } } return r; } /// /// Moves the mouse to the found text (the first word). /// /// X coordinate in the word. Default - center. Examples: 10, ^10 (reverse), .5f (fraction). /// Y coordinate in the word. Default - center. /// Word index offset from . /// area is Bitmap. /// Exceptions of . /// /// Calls . /// public void MouseMove(Coord x = default, Coord y = default, int word = 0) => _MouseAction(x, y, 0, word); /// /// Clicks the found text (the first word). /// /// X coordinate in the word. Default - center. Examples: 10, ^10 (reverse), .5f (fraction). /// Y coordinate in the word. Default - center. /// Which button and how to use it. /// Word index offset from . /// area is Bitmap. /// Exceptions of . /// /// Calls . /// public MRelease MouseClick(Coord x = default, Coord y = default, MButton button = MButton.Left, int word = 0) { _MouseAction(x, y, button == 0 ? MButton.Left : button, word); return button; } /// /// Double-clicks the found text (the first word). /// /// public void MouseClickD(Coord x = default, Coord y = default, int word = 0) => MouseClick(x, y, MButton.DoubleClick, word); /// /// Right-clicks the found text (the first word). /// /// public void MouseClickR(Coord x = default, Coord y = default, int word = 0) => MouseClick(x, y, MButton.Right, word); void _MouseAction(Coord x, Coord y, MButton button, int word) { if (_area.Type == IFArea.AreaType.Bitmap) throw new InvalidOperationException(); var r = GetRect(false, word); if (r.NoArea) return; var p = Coord.NormalizeInRect(x, y, r, centerIfEmpty: true); if (_area.Type == IFArea.AreaType.Screen) { if (button == 0) mouse.move(p); else mouse.clickEx(button, p); } else { var w = _area.W; if (_area.Type == IFArea.AreaType.Elm) { if (!_area.E.GetRect(out var rr, w) || rr.NoArea) throw new AuException(0, "*get rectangle"); p.x += rr.left; p.y += rr.top; } if (button == 0) mouse.move(w, p.x, p.y); else mouse.clickEx(button, w, p.x, p.y); } } /// /// Posts mouse-click messages to the window, using coordinates in the found text (the first word). /// /// X coordinate in the word. Default - center. Examples: 10, ^10 (reverse), .5f (fraction). /// Y coordinate in the word. Default - center. /// Can specify the left (default), right or middle button. Also flag for double-click, press or release. /// Word index offset from . /// area is Bitmap or screen. /// Failed to get UI element rectangle (when searched in a UI element). /// /// Does not move the mouse. /// Does not wait until the target application finishes processing the message. /// Works not with all elements. /// public void PostClick(Coord x = default, Coord y = default, MButton button = MButton.Left, int word = 0) { var w = _area.W; if (w.Is0) throw new InvalidOperationException(); var r = GetRect(false, word); if (r.NoArea) return; if (_area.Type == IFArea.AreaType.Elm) { if (!_area.E.GetRect(out var rr, w) || r.NoArea) throw new AuException(0, "*get rectangle"); r.Offset(rr.left, rr.top); } mouse.PostClick_(w, r, x, y, button); } //rejected: PostClickD, PostClickR. Rarely used. } ================================================ FILE: Au/UI objects/ocrFinder.cs ================================================ using System.Drawing; using System.Drawing.Imaging; using System.Text.RegularExpressions; namespace Au; /// /// Performs OCR (text recognition) and find text in OCR results. Contains text to find and OCR parameters. /// /// /// Can be used instead of . /// public class ocrFinder { readonly object _text; readonly int _textType; readonly OcrFlags _flags; readonly double _scale; readonly IOcrEngine _engine; readonly int _skip; IFArea _area; Hash.MD5Result _md5; bool _waitNot; internal enum Action_ { Find, Wait, WaitNot } /// /// Stores text to find and OCR parameters. /// /// text is empty or contains invalid regular expression. /// public ocrFinder(string text, OcrFlags flags = 0, double scale = 0, IOcrEngine engine = null, int skip = 0) { //**r - regexp (PCRE) //**R - Regex (.NET) //**i - text, ignore case //**t - text _textType = text.Starts(false, "**r ", "**R ", "**i ", "**t "); if (_textType > 0) text = text[4..]; if (string.IsNullOrWhiteSpace(text)) throw new ArgumentException("empty text"); _text = _textType switch { 1 => new regexp(text), 2 => new Regex(text, RegexOptions.CultureInvariant), _ => text }; _flags = flags; _scale = scale; _engine = engine ?? ocr.engine; _skip = skip; } /// /// Returns an object that contains the word index and can click it etc. /// public ocr Result { get; internal set; } /// public ocr Find(IFArea area) => Exists(area) ? Result : null; /// public ocr Find(IFArea area, Seconds wait) => Exists(area, wait) ? Result : null; /// If found, sets and returns true, else false. /// public bool Exists(IFArea area) { _Before(area, Action_.Find); return _Find(false); } /// If found, sets and returns true. Else throws exception or returns false (if wait negative). /// public bool Exists(IFArea area, Seconds wait) { bool r = wait.Exists_() ? Exists(area) : Wait_(Action_.Wait, wait, area); return r || wait.ReturnFalseOrThrowNotFound_(); } /// public ocr Wait(Seconds timeout, IFArea area) => Wait_(Action_.Wait, timeout, area) ? Result : null; //TODO3: suspend waiting while a mouse button is pressed. // Now, eg if finds while scrolling, although MouseMove waits until buttons released, but moves to the old (wrong) place. /// public bool WaitNot(Seconds timeout, IFArea area) => Wait_(Action_.WaitNot, timeout, area); internal bool Wait_(Action_ action, Seconds timeout, IFArea area) { if (area.Type == IFArea.AreaType.Bitmap) throw new ArgumentException("Bitmap and wait"); _Before(area, action); _md5 = default; try { return wait.until(timeout, () => _Find(true) ^ _waitNot); } finally { _area = null; } } //called at the start of _Find and Wait_ void _Before(IFArea area, Action_ action) { Not_.Null(area); area.Before_(_flags.HasAny(OcrFlags.WindowDC | OcrFlags.PrintWindow)); _area = area; _waitNot = action == Action_.WaitNot; } /// /// If testing true, the finder after OCR sets result. Then you can access it when text not found (or found). /// [ThreadStatic] internal static (bool testing, ocr result) testing_; bool _Find(bool waiting) { if (!_area.GetOcrData_(_flags, out var b, out var resultOffset)) return false; bool inBitmap = _area.Type == IFArea.AreaType.Bitmap; if (waiting) { //don't OCR if nothing changed. Can be expensive ($) if using cloud. var m = _BitmapHash(b); if (m == _md5) { if (!inBitmap) b.Dispose(); return _waitNot; } _md5 = m; } var scale = _area.GetOcrScale_(_scale, _engine); var a = _engine.Recognize(b, dispose: !inBitmap, scale); var r = new ocr(a, _area); if (testing_.testing) testing_.result = r; if (!_FindText(r)) return false; r.AdjustResults_(resultOffset, _flags); Result = r; return true; static unsafe Hash.MD5Result _BitmapHash(Bitmap b) { using var d = b.Data(ImageLockMode.ReadOnly); return Hash.MD5(new RByte((void*)d.Scan0, d.Height * d.Stride)); } } bool _FindText(ocr r) { string s1 = r.TextForFind, s2 = _text as string; if (s2 != null) { s1 = s1.Replace('I', 'l').Replace('1', 'l').Replace('0', 'O'); s2 = s2.Replace('I', 'l').Replace('1', 'l').Replace('0', 'O'); } int startIndex = 0, skip = _skip; g1: int i = -1, i2 = i; switch (_text) { case regexp rx: if (rx.Match(s1, 0, out RXGroup g, startIndex..)) { i = g.Start; i2 = g.End; } break; case Regex rx: if (rx.Match(s1, startIndex) is var m && m.Success) { i = m.Index; i2 = i + m.Length; } break; default: i = s1.Find(s2, startIndex, _textType == 3); i2 = i + s2.Length; break; } if (i >= 0) { if (skip > 0) { skip--; startIndex = i2; goto g1; } r.FoundTextRange = new(i, i2); for (int j = r.Words.Length; --j >= 0;) { if (r.Words[j].Offset <= i) { r.FoundWordIndex = j; return true; } } } return false; } /// public override string ToString() => _text.ToString(); } ================================================ FILE: Au/UI objects/ocr_types.cs ================================================ using System.Drawing; using System.Drawing.Imaging; using System.Text.Json.Nodes; namespace Au.Types; /// /// Used by and . /// public interface IOcrEngine { /// /// Recognizes text in image. /// /// Image. /// Call b.Dispose(). The image will not be used by the caller. /// Scale factor. /// Recognized words. /// /// The class that implements this function can use static functions to prepare the image (scale etc) and get image data. /// OcrWord[] Recognize(Bitmap b, bool dispose, double scale); /// /// Let OCR functions scale images captured from a window or screen, 1-2 times depending on DPI, unless non-zero scale parameter is specified. /// bool DpiScale { get; set; } #region util /// /// If need, resizes the image and/or ensures it isn't too small. /// /// Call b.Dispose() if created new image. The input image will not be used by the caller. /// Scale factor. /// Minimal image width accepted by the OCR engine. /// Minimal image height accepted by the OCR engine. protected static Bitmap PrepareBitmap(Bitmap b, bool dispose, double scale, int minWidth = 0, int minHeight = 0) { var b0 = b; if (scale > 1) b = b.Resize(scale, BRFilter.Lanczos3, dispose); //Microsoft OCR (Win10 and Azure) reject small images if (minWidth > 0 && minHeight > 0) { var zi = b.Size; if (zi.Width < minWidth || zi.Height < minHeight) { int wid = Math.Max(zi.Width, minWidth), hei = Math.Max(zi.Height, minHeight); var b2 = new Bitmap(wid, hei, b.PixelFormat); b2.SetResolution(b.HorizontalResolution, b.VerticalResolution); using var g = Graphics.FromImage(b2); g.Clear(Color.White); g.DrawImage(b, 0, 0); if (dispose || b != b0) b.Dispose(); b = b2; } } //var file = folders.Temp + "test.png"; b.Save(file); run.it(file); return b; } /// /// Gets image pixels. /// /// The pixel format is either Format32bppRgb (if it's b format) or Format32bppArgb. protected static unsafe byte[] GetBitmapData(Bitmap b) { using var d = b.Data(ImageLockMode.ReadOnly, b.PixelFormat == PixelFormat.Format32bppRgb ? PixelFormat.Format32bppRgb : PixelFormat.Format32bppArgb); return new Span((void*)d.Scan0, d.Width * d.Height * 4).ToArray(); } /// /// Gets image data in .png file format. /// protected static unsafe byte[] GetBitmapPngFileData(Bitmap b) { using var ms = new MemoryStream(); b.Save(ms, ImageFormat.Png); var a = ms.ToArray(); return a; } /// /// Converts word polygon JSON nodes to . /// protected static RECT PolyToRect(JsonNode xTL, JsonNode yTL, JsonNode xTR, JsonNode yTR, JsonNode xBR, JsonNode yBR, JsonNode xBL, JsonNode yBL) { return RECT.FromLTRB((i(xTL) + i(xBL) + 1) / 2, (i(yTL) + i(yTR) + 1) / 2, (i(xTR) + i(xBR)) / 2, (i(yBR) + i(yBL)) / 2); static int i(JsonNode n) => n != null ? (int)n : 0; //the node may be missing in JSON if has default value } #endregion } /// /// Stores text and rectangle of a word in OCR results. /// See . /// public record class OcrWord { /// Separator before the word (space, new line, etc). Also can be "" or null. /// Word text. /// Word rectangle in area, possibly scaled. /// The scale parameter of the OCR function. This function unscales rect if need. public OcrWord(string separator, string text, RECT rect, double scale) { Separator = separator; Text = text; Rect = scale > 1 ? RECT.FromLTRB(i(rect.left), i(rect.top), i(rect.right), i(rect.bottom)) : rect; int i(int x) => (x / scale).ToInt(); } /// Separator before the word (space, new line, etc). Also can be "" or null. public string Separator { get; } /// Word text. public string Text { get; } /// Word rectangle in area. public RECT Rect { get; internal set; } /// Word offset in . public int Offset { get; internal set; } } /// /// Flags for and . /// [Flags] public enum OcrFlags { /// WindowDC = 1, /// PrintWindow = 2, ///// //WindowDwm = 4, //note: the above values must be the same in CIFlags, CIUFlags, IFFlags, OcrFlags. //rejected. Maybe in the future. ///// ///// Sort words by word rectangle position, left-to-right and top-to-bottom. ///// //Sort = 0x100, } ================================================ FILE: Au/UI objects/uiimage.cs ================================================ using System.Drawing; namespace Au; /// /// Captures, finds and clicks images and colors in windows. /// /// /// An image is any visible rectangular part of a window. A color is any visible pixel (the same as image of size 1x1). /// A uiimage variable holds results of and similar functions (rectangle etc). /// public class uiimage { #region results readonly IFArea _area; ///// ///// area parameter of the function. ///// //public IFArea Area => _area; internal uiimage(IFArea area) { _area = area; } /// /// Gets location of the found image, relative to the search area. /// /// /// Relative to the window/control client area (if area type is ), UI element (if ), image (if ) or screen (if ). /// More info: . /// public RECT Rect { get; init; } /// /// Gets location of the found image in screen coordinates. /// /// /// Slower than . /// public RECT RectInScreen { get { RECT r; switch (_area.Type) { case IFArea.AreaType.Wnd: r = Rect; _area.W.MapClientToScreen(ref r); return r; case IFArea.AreaType.Elm: if (!_area.E.GetRect(out var rr)) return default; r = Rect; r.Offset(rr.left, rr.top); return r; } return Rect; //screen or bitmap } } /// /// Gets 0-based index of current matching image instance. /// /// /// Can be useful in also callback functions. /// When the image argument is a list of images, MatchIndex starts from 0 for each list image. /// public int MatchIndex { get; init; } /// /// When the image argument is a list of images, gets 0-based index of the list image. /// public int ListIndex { get; init; } /// /// Can be used in also callback function to skip n matching images. Example: also: o => o.Skip(n). /// /// How many matching images to skip. public IFAlso Skip(int n) => MatchIndex == n ? IFAlso.OkReturn : (MatchIndex < n ? IFAlso.FindOther : IFAlso.FindOtherOfList); /// /// Moves the mouse to the found image. /// /// X coordinate in the found image. Default - center. Examples: 10, ^10 (reverse), .5f (fraction). /// Y coordinate in the found image. Default - center. /// area is Bitmap. /// Exceptions of . /// /// Calls . /// public void MouseMove(Coord x = default, Coord y = default) => _MouseAction(x, y, 0); /// /// Clicks the found image. /// /// X coordinate in the found image. Default - center. Examples: 10, ^10 (reverse), .5f (fraction). /// Y coordinate in the found image. Default - center. /// Which button and how to use it. /// area is Bitmap. /// Exceptions of . /// /// Calls . /// public MRelease MouseClick(Coord x = default, Coord y = default, MButton button = MButton.Left) { _MouseAction(x, y, button == 0 ? MButton.Left : button); return button; } /// /// Double-clicks the found image. /// /// public void MouseClickD(Coord x = default, Coord y = default) => MouseClick(x, y, MButton.DoubleClick); /// /// Right-clicks the found image. /// /// public void MouseClickR(Coord x = default, Coord y = default) => MouseClick(x, y, MButton.Right); void _MouseAction(Coord x, Coord y, MButton button) { if (_area.Type == IFArea.AreaType.Bitmap) throw new InvalidOperationException(); Debug.Assert(!Rect.NoArea); if (Rect.NoArea) return; //rejected: Click will activate it. Don't activate if just Move. //if(0 != (_f._flags & IFFlags.WindowDC)) { // if(_area.W.IsCloaked) _area.W.ActivateL(); //} var p = Coord.NormalizeInRect(x, y, Rect, centerIfEmpty: true); if (_area.Type == IFArea.AreaType.Screen) { if (button == 0) mouse.move(p); else mouse.clickEx(button, p); } else { var w = _area.W; if (_area.Type == IFArea.AreaType.Elm) { if (!_area.E.GetRect(out var r, w) || r.NoArea) throw new AuException(0, "*get rectangle"); p.x += r.left; p.y += r.top; } if (button == 0) mouse.move(w, p.x, p.y); else mouse.clickEx(button, w, p.x, p.y); } } /// /// Posts mouse-click messages to the window, using coordinates in the found image. /// /// Can specify the left (default), right or middle button. Also flag for double-click, press or release. /// Unsupported button specified. /// public void PostClick(Coord x = default, Coord y = default, MButton button = MButton.Left) { var w = _area.W; if (w.Is0) throw new InvalidOperationException(); Debug.Assert(!Rect.NoArea); if (Rect.NoArea) return; var r = Rect; if (_area.Type == IFArea.AreaType.Elm) { if (!_area.E.GetRect(out var rr, w) || r.NoArea) throw new AuException(0, "*get rectangle"); r.Offset(rr.left, rr.top); } mouse.PostClick_(w, r, x, y, button); } /// /// Posts mouse-double-click messages to the window, using coordinates in the found image. /// /// X coordinate in the found image. Default - center. Examples: 10, ^10 (reverse), .5f (fraction). /// Y coordinate in the found image. Default - center. /// area is Bitmap or screen. /// Failed to get UI element rectangle (when searched in a UI element). /// /// Does not move the mouse. /// Does not wait until the target application finishes processing the message. /// Works not with all elements. /// public void PostClickD(Coord x = default, Coord y = default) => PostClick(x, y, MButton.DoubleClick); /// /// Posts mouse-right-click messages to the window, using coordinates in the found image. /// /// public void PostClickR(Coord x = default, Coord y = default) => PostClick(x, y, MButton.Right); /// public override string ToString() => $"{ListIndex}, {MatchIndex}, {Rect}"; #endregion /// /// Finds image(s) or color(s) displayed in a window or other area. /// /// /// Returns a object that contains the rectangle of the found image and can click it etc. /// Returns null if not found. /// /// /// Where to search: ///
- window or control (its client area). ///
- UI element. ///
- image. ///
- a rectangle area in screen. ///
- can contain , or . Also allows to specify a rectangle in it, which makes the area smaller and the function faster. Example: new(w, (left, top, width, height)). /// /// Image or color to find. Or array of them. More info: . /// /// Maximal allowed color difference. Can be 0 - 100, but should be as small as possible. Use to find images with slightly different colors than the specified image. /// /// Callback function. Called for each found image instance and receives its rectangle, match index and list index. Can return one of values. ///
Examples: ///
• Skip 2 matching images: also: o => o.Skip(2) ///
• Skip some matching images if some condition is false: also: o => condition ? IFAlso.OkReturn : IFAlso.FindOther ///
• Get rectangles etc of all matching images: also: o => { list.Add(o); return IFAlso.OkFindMore; } ///
• Do different actions depending on which list images found: var found = new BitArray(images.Length); uiimage.find(w, images, also: o => { found[o.ListIndex] = true; return IFAlso.OkFindMoreOfList; }); if(found[0]) print.it(0); if(found[1]) print.it(1); /// /// Invalid window handle (the area argument). /// An argument is/contains a null/invalid value. /// Image file does not exist. /// Exceptions of . /// Something failed. /// /// To create code for this function, use tool Find image or color in window. /// /// The speed mostly depends on: /// 1. The size of the search area. Use the smallest possible area (control or UI element or rectangle in window). /// 2. Flags (makes faster), . The speed depends on window. /// 3. Video driver. Can be much slower if incorrect, generic or virtual PC driver is used. The above flags should help. /// 4. diff. Should be as small as possible. /// /// If flag or not used, the search area must be visible on the screen, because this function then gets pixels from the screen. /// /// Can find only images that exactly match the specified image. With diff can find images with slightly different colors and brightness. /// /// Transparent and partially transparent pixels of image are ignored. You can draw transparent areas with an image editor that supports it, for example Paint.NET. /// /// This function is not the best way to find objects when the script is intended for long use or for use on multiple computers or must be very reliable. Because it may fail to find the image after changing some settings - system theme, application theme, text size (DPI), font smoothing (if the image contains text), etc. Also are possible various unexpected temporary conditions that may distort or hide the image, for example adjacent window shadow, a tooltip or some temporary window. If possible, in such scripts instead use other functions, eg find control or UI element. /// /// Flags and cannot be used if area is Bitmap or . /// /// /// Code created with tool Find image or color in window. /// /// public static uiimage find(IFArea area, IFImage image, IFFlags flags = 0, int diff = 0, Func also = null) => new uiimageFinder(image, flags, diff, also).Find(area); /// /// Finds image(s) or color(s) displayed in a window or other area. Can wait and throw . /// /// /// Returns a object that contains the rectangle of the found image and can click it etc. /// If not found, throws exception or returns null (if wait negative). /// /// The wait timeout, seconds. If 0, does not wait. If negative, does not throw exception when not found. /// /// Invalid window handle (the area argument), or the window closed while waiting. /// public static uiimage find(Seconds wait, IFArea area, IFImage image, IFFlags flags = 0, int diff = 0, Func also = null) => new uiimageFinder(image, flags, diff, also).Find(area, wait); /// /// Finds image(s) or color(s) displayed in a window or other area. Waits until found. /// More info: . /// /// Returns object containing the rectangle of the found image. On timeout returns null if timeout is negative; else exception. /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// timeout time has expired (if > 0). /// Invalid window handle (the area argument), or the window closed while waiting. /// public static uiimage wait(Seconds timeout, IFArea area, IFImage image, IFFlags flags = 0, int diff = 0, Func also = null) => new uiimageFinder(image, flags, diff, also).Wait(timeout, area); /// /// Waits until image(s) or color(s) is not displayed in a window or other area. /// More info: . /// /// Returns true. On timeout returns false if timeout is negative; else exception. /// public static bool waitNot(Seconds timeout, IFArea area, IFImage image, IFFlags flags = 0, int diff = 0, Func also = null) => new uiimageFinder(image, flags, diff, also).WaitNot(timeout, area); /// /// Waits until something visually changes in a window or other area. /// More info: . /// /// /// Like , but instead of image parameter this function captures the area image at the beginning. /// /// public static bool waitChanged(Seconds timeout, IFArea area, IFFlags flags = 0, int diff = 0) { var f = new uiimageFinder(default, flags, diff, null); return f.Wait_(uiimageFinder.Action_.WaitChanged, timeout, area); } #region obsolete /// [Obsolete("Use CaptureScreen.Image"), EditorBrowsable(EditorBrowsableState.Never)] public static Bitmap capture(RECT r) => CaptureScreen.Image(r); /// [Obsolete("Use CaptureScreen.Image"), EditorBrowsable(EditorBrowsableState.Never)] public static Bitmap capture(wnd w, RECT r, bool printWindow = false) => CaptureScreen.Image(w, r, printWindow ? CIFlags.PrintWindow : CIFlags.WindowDC); /// [Obsolete("Use CaptureScreen.Pixels"), EditorBrowsable(EditorBrowsableState.Never)] public static uint[,] getPixels(RECT r) => CaptureScreen.Pixels(r); /// [Obsolete("Use CaptureScreen.Pixels"), EditorBrowsable(EditorBrowsableState.Never)] public static uint[,] getPixels(wnd w, RECT r, bool printWindow = false) => CaptureScreen.Pixels(w, r, printWindow ? CIFlags.PrintWindow : CIFlags.WindowDC); /// [Obsolete("Use CaptureScreen.Pixel"), EditorBrowsable(EditorBrowsableState.Never)] public static unsafe uint getPixel(POINT p) => CaptureScreen.Pixel(p); /// [Obsolete("Use CaptureScreen.ImageColorRectUI"), EditorBrowsable(EditorBrowsableState.Never)] public static bool captureUI(out ICResult result, ICFlags flags = 0, AnyWnd owner = default) { bool R = CaptureScreen.ImageColorRectUI(out var r1, (CIUFlags)flags, owner); result = Unsafe.As(r1); return R; } #endregion } ================================================ FILE: Au/UI objects/uiimageFinder.cs ================================================ //FUTURE: option to allow % of image completely different. Eg button with/without focus rectangle. // Or/and in the tool allow to erase some areas (make alpha 0). //#define WI_TEST_NO_OPTIMIZATION using System.Drawing; using System.Drawing.Imaging; namespace Au; /// /// Finds images displayed in user interface (UI). Contains data and parameters of image(s) or color(s) to find. /// /// /// Can be used instead of . /// public unsafe class uiimageFinder { class _Image { public uint[] pixels; public int width, height; public _OptimizationData optim; public _Image(string file) { using var b = ImageUtil.LoadGdipBitmap(file); _BitmapToData(b); } public _Image(Bitmap b) { b = b ?? throw new ArgumentException("null Bitmap"); _BitmapToData(b); } void _BitmapToData(Bitmap b) { var z = b.Size; width = z.Width; height = z.Height; pixels = new uint[width * height]; fixed (uint* p = pixels) { var d = new BitmapData { Scan0 = (IntPtr)p, Height = height, Width = width, Stride = width * 4, PixelFormat = PixelFormat.Format32bppArgb }; d = b.LockBits(new Rectangle(default, z), ImageLockMode.ReadOnly | ImageLockMode.UserInputBuffer, PixelFormat.Format32bppArgb, d); b.UnlockBits(d); if (d.Stride < 0) throw new ArgumentException("bottom-up Bitmap"); //Image.FromHbitmap used to create bottom-up bitmap (stride<0) from compatible bitmap. Now cannot reproduce. } } public _Image(ColorInt color) { width = height = 1; pixels = new uint[1] { (uint)color.argb | 0xff000000 }; } public _Image() { } } //ctor parameters readonly List<_Image> _images; //support multiple images readonly IFFlags _flags; readonly uint _diff; readonly Func _also; Action_ _action; IFArea _area; CaptureScreenImage _ad; //area data POINT _resultOffset; //to map the found rectangle from the captured area coordinates to the specified area coordinates /// /// Returns object that contains the rectangle of the found image and can click it etc. /// public uiimage Result { get; private set; } /// /// Stores image/color data and search settings in this object. Loads images if need. See . /// /// An argument is/contains a null/invalid value. /// Image file does not exist. /// Exceptions of . /// public uiimageFinder(IFImage image, IFFlags flags = 0, int diff = 0, Func also = null) { _flags = flags; uint d = (uint)diff; _diff = d switch { <= 30 => d, <= 60 => 30 + (d - 30) * 2, <= 100 => 90 + (d - 60) * 3, _ => throw new ArgumentOutOfRangeException("diff range: 0 - 100") }; //make slightly exponential, 0 - 210 _also = also; _images = new List<_Image>(); if (image.Value != null) _AddImage(image); void _AddImage(IFImage image) { switch (image.Value) { case string s: _images.Add(new _Image(s)); break; case Bitmap b: _images.Add(new _Image(b)); break; case ColorInt c: _images.Add(new _Image(c)); break; case IFImage[] a: foreach (var v in a) _AddImage(v); break; } } } /// /// Finds the first image displayed in the specified window or other area. /// See . /// /// If found, returns , else null. /// Invalid window handle. /// An argument of this function or of constructor is invalid. /// Something failed. /// /// Functions Find and differ only in their return types. /// /// public uiimage Find(IFArea area) => Exists(area) ? Result : null; /// /// Finds the first image displayed in the specified window or other area. Can wait and throw . /// /// If found, returns . Else throws exception or returns null (if wait negative). /// The wait timeout, seconds. If 0, does not wait. If negative, does not throw . /// Invalid window handle. /// An argument of this function or of constructor is invalid. /// Something failed. /// /// /// Functions Find and differ only in their return types. /// /// public uiimage Find(IFArea area, Seconds wait) => Exists(area, wait) ? Result : null; /// If found, sets and returns true, else false. /// public bool Exists(IFArea area) { _Before(area, Action_.Find); return _Find(); } /// If found, sets and returns true. Else throws exception or returns false (if wait negative). /// public bool Exists(IFArea area, Seconds wait) { bool r = wait.Exists_() ? Exists(area) : Wait_(Action_.Wait, wait, area); return r || wait.ReturnFalseOrThrowNotFound_(); } /// /// See . /// /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// Exceptions of , except those of the constructor. /// /// Same as , except: /// - 0 timeout means infinite. /// - on timeout throws , not . /// /// public uiimage Wait(Seconds timeout, IFArea area) => Wait_(Action_.Wait, timeout, area) ? Result : null; //TODO3: suspend waiting while a mouse button is pressed. // Now, eg if finds while scrolling, although MouseMove waits until buttons released, but moves to the old (wrong) place. /// /// See . /// /// Exceptions of , except those of the constructor. /// public bool WaitNot(Seconds timeout, IFArea area) => Wait_(Action_.WaitNot, timeout, area); internal bool Wait_(Action_ action, Seconds timeout, IFArea area) { if (area.Type == IFArea.AreaType.Bitmap) throw new ArgumentException("Bitmap and wait"); _Before(area, action); try { return wait.until(timeout, () => _Find() ^ (action > Action_.Wait)); } finally { _After(); } //tested: does not create garbage while waiting. } internal enum Action_ { Find, Wait, WaitNot, WaitChanged } //called at the start of _Find and Wait_ void _Before(IFArea area, Action_ action) { Not_.Null(area); _action = action; _area = area; _ad ??= new(); if (_action == Action_.WaitChanged) { Debug.Assert(_images.Count == 0 && _also == null); //the first _Find will capture the area and add to _images } else { if (_images.Count == 0) throw new ArgumentException("no image"); } _area.Before_(_flags.HasAny(IFFlags.WindowDC | IFFlags.PrintWindow)); } //called at the end of _Find (if not waiting) and Wait_ void _After() { _ad.Dispose(); _area = null; if (_action == Action_.WaitChanged) _images.Clear(); } bool _Find() { //using var p1 = perf.local(); Result = null; if (!_area.GetRect_(out var r, out _resultOffset, _flags)) return false; //If WaitChanged, first time just get area pixels into _images[0]. if (_action == Action_.WaitChanged && _images.Count == 0) { return _GetAreaPixels(r, true); } //Return false if all images are bigger than the search area. for (int i = _images.Count; --i >= 0;) { var v = _images[i]; if (v.width <= r.Width && v.height <= r.Height) goto g1; } return false; g1: BitmapData bitmapBD = null; try { //Get area pixels. bool havePixels = _area.Type == IFArea.AreaType.Bitmap; if (havePixels) { var pf = (_area.B.PixelFormat == PixelFormat.Format32bppArgb) ? PixelFormat.Format32bppArgb : PixelFormat.Format32bppRgb; //if possible, use PixelFormat of _area, to avoid conversion/copying. Both these formats are ok, we don't use alpha. bitmapBD = _area.B.LockBits(r, ImageLockMode.ReadOnly, pf); if (bitmapBD.Stride < 0) throw new ArgumentException("bottom-up Bitmap"); _ad.SetExternalData_((uint*)bitmapBD.Scan0, bitmapBD.Width, bitmapBD.Height); //note: don't support rect in Bitmap. LockBits does not copy bits if same pixelformat. Would need to create new Bitmap from that rect, or use stride when searching. } else { havePixels = _GetAreaPixels(r); } //p1.Next(); if (havePixels) { //Find image(s) in area. uiimage alsoResult = null; _DpiScaling dpiScaling = null; if (_action == Action_.WaitChanged) return _FindImage(0, ref alsoResult, ref dpiScaling); if (_flags.Has(IFFlags.Parallel) && _images.Count > 1) { Parallel.For(0, _images.Count, (i, pls) => { _FindImage(i, ref alsoResult, ref dpiScaling, pls); }); } else { for (int i = 0; i < _images.Count; i++) { if (_FindImage(i, ref alsoResult, ref dpiScaling)) break; } } Result ??= alsoResult; if (Result != null) return true; } return false; } finally { if (bitmapBD != null) _area.B.UnlockBits(bitmapBD); if (_action == Action_.Find) _After(); } } [MethodImpl(MethodImplOptions.AggressiveOptimization)] bool _FindImage(int listIndex, ref uiimage alsoResult, ref _DpiScaling dpiScaling, ParallelLoopState pls = null) { //note: can run in multiple threads simultaneously. Don't modify this fields and ref params without lock. var image = _images[listIndex]; var alsoAction = IFAlso.FindOtherOfList; int matchIndex = 0; int imageWidth = image.width, imageHeight = image.height; if (_ad.Width < imageWidth || _ad.Height < imageHeight) return false; fixed (uint* imagePixels = image.pixels) { uint* imagePixelsTo = imagePixels + imageWidth * imageHeight; uint* areaPixels = _ad.Pixels; //rejected: if image is of same size as area, simply compare. For example, when action is WaitChanged. // Does not make faster, just adds more code. if (!image.optim.Init(image, _ad.Width)) return false; var optim = image.optim; //copy struct, size = 9*int int o_pos0 = optim.v0.pos; var o_a1 = &optim.v1; var o_an = o_a1 + (optim.N - 1); //find first pixel. This part is very important for speed. //int nTimesFound = 0; //debug var areaWidthMinusImage = _ad.Width - imageWidth; var pFirst = areaPixels + o_pos0; var pLast = pFirst + _ad.Width * (_ad.Height - imageHeight) + areaWidthMinusImage; //this is a workaround for compiler not using registers for variables in fast loops (part 1) var f = new _FindData { color = (optim.v0.color & 0xffffff) | (_diff << 24), p = pFirst - 1, pLineLast = pFirst + areaWidthMinusImage }; #region fast_code //This for loop must be as fast as possible. // There are too few 32-bit registers. Must be used a many as possible registers. See comments below. // No problems if 64-bit. gContinue: if (pls?.IsStopped ?? false) goto gNotFound; { var f_ = &f; //part 2 of the workaround var p_ = f_->p + 1; //register var color_ = f_->color; //register var pLineLast_ = f_->pLineLast; //register for (; ; ) { //lines if (color_ < 0x1000000) { for (; p_ <= pLineLast_; p_++) { if (color_ == (*p_ & 0xffffff)) goto gPixelFound; } } else { //all variables except f.pLineLast are in registers // It is very sensitive to other code. Compiler can take some registers for other code and not use here. // Then still not significantly slower, but I like to have full speed. // Code above fast_code region should not contain variables that are used in loops below this block. // Also don't use class members in fast_code region, because then compiler may take a register for 'this' pointer. // Here we use f.pLineLast instead of pLineLast_, else d2_ would be in memory (it is used 3 times). var d_ = color_ >> 24; //register var d2_ = d_ * 2; //register for (; p_ <= f.pLineLast; p_++) { if ((color_ & 0xff) - ((byte*)p_)[0] + d_ > d2_) continue; if ((color_ >> 8 & 0xff) - ((byte*)p_)[1] + d_ > d2_) continue; if ((color_ >> 16 & 0xff) - ((byte*)p_)[2] + d_ > d2_) continue; goto gPixelFound; } } if (p_ > pLast) goto gNotFound; p_--; p_ += imageWidth; f.pLineLast = pLineLast_ = p_ + areaWidthMinusImage; } gPixelFound: f.p = p_; } //nTimesFound++; var ap = f.p - o_pos0; //the first area pixel of the top-left of the image //compare other 0-3 selected pixels for (var op = o_a1; op < o_an; op++) { uint aPix = ap[op->pos], iPix = op->color; var colorDiff = f.color >> 24; if (colorDiff == 0) { if (!_MatchPixelExact(aPix, iPix)) goto gContinue; } else { if (!_MatchPixelDiff(aPix, iPix, colorDiff)) goto gContinue; } } //now compare all pixels of the image //perf.first(); uint* ip = imagePixels, ipLineTo = ip + imageWidth; for (; ; ) { //lines if (f.color < 0x1000000) { do { if (!_MatchPixelExact(*ap, *ip)) goto gContinue; ap++; } while (++ip < ipLineTo); } else { var colorDiff = f.color >> 24; do { if (!_MatchPixelDiff(*ap, *ip, colorDiff)) goto gContinue; ap++; } while (++ip < ipLineTo); } if (ip == imagePixelsTo) break; ap += areaWidthMinusImage; ipLineTo += imageWidth; } //perf.nw(); //print.it(nTimesFound); #endregion if (_action != Action_.WaitChanged) { int iFound = (int)(f.p - o_pos0 - areaPixels); RECT r = new(iFound % _ad.Width, iFound / _ad.Width, imageWidth, imageHeight); lock (this) { if (pls?.IsStopped ?? false) goto gNotFound; if (_flags.HasAny(IFFlags.WindowDC | IFFlags.PrintWindow)) { dpiScaling ??= new(_area.W); dpiScaling.ScaleRect(ref r); } r.Offset(_resultOffset.x, _resultOffset.y); uiimage tempResult = new(_area) { Rect = r, MatchIndex = matchIndex, ListIndex = listIndex }; if (_also != null) { alsoAction = _also(tempResult); if (alsoAction is IFAlso.OkFindMoreOfThis or IFAlso.FindOtherOfThis && pls != null) { //stop other threads, but not this thread pls.Stop(); pls = null; //never mind: if later _also returns "continue to search in list" (unlikely), will not search. // pls.Stop must be called while locked, to prevent other threads calling _also afterwards. // Now using the simple way (lock). // The complex way - use Monitor.Enter/Exit. Call Stop/Exit when returning. Bad: other threads would continue to search unnecessarily. } switch (alsoAction) { case IFAlso.OkFindMore or IFAlso.OkFindMoreOfThis: alsoResult = tempResult; matchIndex++; goto gContinue; case IFAlso.FindOther or IFAlso.FindOtherOfThis: matchIndex++; goto gContinue; case IFAlso.OkFindMoreOfList: alsoResult = tempResult; return false; case IFAlso.FindOtherOfList: return false; } } if (alsoAction != IFAlso.NotFound) Result = tempResult; pls?.Stop(); } } } //fixed return true; gNotFound: return alsoAction is IFAlso.FindOtherOfThis or IFAlso.OkFindMoreOfThis; //returns true to stop seaching (skip other images in list) } struct _FindData { public uint color; public uint* p, pLineLast; } [MethodImpl(MethodImplOptions.AggressiveInlining)] static bool _MatchPixelExact(uint ap, uint ip) { if (ip == (ap | 0xff000000)) return true; return ip < 0xff000000; //transparent? } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] static bool _MatchPixelDiff(uint ap, uint ip, uint colorDiff) { //info: optimized. Don't modify. // All variables are in registers. // Only 3.5 times slower than _MatchPixelExact (when all pixels match), which is inline. if (ip >= 0xff000000) { //else transparent uint d = colorDiff, d2 = d * 2; if (((ip & 0xff) - (ap & 0xff) + d) > d2) goto gFalse; if (((ip >> 8 & 0xff) - (ap >> 8 & 0xff) + d) > d2) goto gFalse; if (((ip >> 16 & 0xff) - (ap >> 16 & 0xff) + d) > d2) goto gFalse; } return true; gFalse: return false; } //bool _CompareSameSize(uint* area, uint* image, uint* imageTo, uint colorDiff) //{ // if(colorDiff == 0) { // do { // if(!_MatchPixelExact(*area, *image)) break; // area++; // } while(++image < imageTo); // } else { // do { // if(!_MatchPixelDiff(*area, *image, colorDiff)) break; // area++; // } while(++image < imageTo); // } // return image == imageTo; //} static bool _IsTransparent(uint color) => color < 0xff000000; struct _OptimizationData { internal struct POSCOLOR { public int pos; //the position in area (not in image) from which to start searching. Depends on where in the image is the color. public uint color; }; #pragma warning disable 649 //never assigned public POSCOLOR v0, v1, v2, v3; //POSCOLOR[] would be slower #pragma warning restore 649 public int N; //A valid count int _areaWidth; public bool Init(_Image image, int areaWidth) { if (areaWidth != _areaWidth) { _areaWidth = areaWidth; N = 0; } if (N != 0) return N > 0; int imageWidth = image.width, imageHeight = image.height; int imagePixelCount = imageWidth * imageHeight; var imagePixels = image.pixels; int i; #if WI_TEST_NO_OPTIMIZATION _Add(image, 0, areaWidth); #else //Find several unique-color pixels for first-pixel search. //This greatly reduces the search time in most cases. //find first nontransparent pixel for (i = 0; i < imagePixelCount; i++) if (!_IsTransparent(imagePixels[i])) break; if (i == imagePixelCount) { N = -1; return false; } //not found because all pixels in image are transparent //find first nonbackground pixel (consider top-left pixel is background) bool singleColor = false; if (i == 0) { i = _FindDifferentPixel(0); if (i < 0) { singleColor = true; i = 0; } } _Add(image, i, areaWidth); if (!singleColor) { //find second different pixel int i0 = i; i = _FindDifferentPixel(i); if (i >= 0) { _Add(image, i, areaWidth); //find other different pixels fixed (POSCOLOR* p = &v0) { while (N < 4) { for (++i; i < imagePixelCount; i++) { var c = imagePixels[i]; if (_IsTransparent(c)) continue; int j = N - 1; for (; j >= 0; j--) if (c == p[j].color) break; //find new color if (j < 0) break; //found } if (i >= imagePixelCount) break; _Add(image, i, areaWidth); } } } else { for (i = imagePixelCount - 1; i > i0; i--) if (!_IsTransparent(imagePixels[i])) break; _Add(image, i, areaWidth); } } //fixed (POSCOLOR* o_pc = &v0) for(int j = 0; j < N; j++) print.it($"{o_pc[j].pos} 0x{o_pc[j].color:X}"); #endif return true; int _FindDifferentPixel(int iCurrent) { int m = iCurrent, n = imagePixelCount; uint notColor = imagePixels[m++]; for (; m < n; m++) { var c = imagePixels[m]; if (c == notColor || _IsTransparent(c)) continue; return m; } return -1; } } void _Add(_Image image, int i, int areaWidth) { fixed (POSCOLOR* p0 = &v0) { var p = p0 + N++; p->color = image.pixels[i]; int w = image.width, x = i % w, y = i / w; p->pos = y * areaWidth + x; } } } bool _GetAreaPixels(RECT r, bool toImage0 = false) { //Transfer from screen/window DC to memory DC (does not work without this) and get pixels. //This is the slowest part of Find, especially BitBlt. //Speed depends on computer, driver, OS version, theme, size. //For example, with Aero theme 2-15 times slower (on Windows 8/10 cannot disable Aero). //With incorrect/generic video driver can be 10 times slower. Eg on vmware virtual PC. //Much faster when using window DC. Then same speed as without Aero. _ad.DontSetAlpha_ = !toImage0; bool ok = _area.Type == IFArea.AreaType.Screen ? _ad.Capture(r, relaxed: true) : _ad.Capture(_area.W, r, _flags.ToCIFlags_() | CIFlags.Relaxed); if (!ok) return false; //r not in client area. Probably the window resized since r.Intersect(clientArea). if (toImage0) { var im = new _Image { width = r.Width, height = r.Height, pixels = _ad.ToArray1D() }; _images.Add(im); } return true; } #if false //r is relative to the search area void _DpiScaleRect(ref RECT r) { if (!_flags.HasAny(IFFlags.WindowDC | IFFlags.PrintWindow)) return; var w = _area.W.Window; if (!Dpi.IsWindowVirtualizedWin10_(w)) return; //makes faster on Win10+; don't scale on older OS int d1 = screen.of(w).Dpi, d2 = Dpi.OfWindow(w); r.left = Math2.MulDiv(r.left, d1, d2); r.top = Math2.MulDiv(r.top, d1, d2); r.right = Math2.MulDiv(r.right, d1, d2); r.bottom = Math2.MulDiv(r.bottom, d1, d2); } #else //cache DPI scaling info. Getting it can make much slower if many matches found. Above is the non-cached version. class _DpiScaling { wnd _w; ushort _dScreen, _dWindow; bool _inited, _scaled; public _DpiScaling(wnd w) { _w = w; } //r is relative to the search area public void ScaleRect(ref RECT r) { if (!_inited) { _inited = true; if (_scaled = Dpi.IsWindowVirtualizedWin10_(_w = _w.Window)) { _dScreen = (ushort)screen.of(_w).Dpi; _dWindow = (ushort)Dpi.OfWindow(_w); } } if (_scaled) { r.left = Math2.MulDiv(r.left, _dScreen, _dWindow); r.top = Math2.MulDiv(r.top, _dScreen, _dWindow); r.right = Math2.MulDiv(r.right, _dScreen, _dWindow); r.bottom = Math2.MulDiv(r.bottom, _dScreen, _dWindow); } } } #endif } ================================================ FILE: Au/UI objects/uiimage_types.cs ================================================ using System.Drawing; namespace Au.Types; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member /// /// Defines the search area for and similar functions. /// /// /// It can be a window/control, UI element, image or a rectangle in screen. /// Has implicit conversions from , , and (rectangle in screen). /// Constructors can be used to specify a rectangle in window or UI element, which makes the area smaller and the function faster. /// Example: uiimage.find(new(w, (left, top, width, height)), image);. /// public class IFArea { internal enum AreaType : byte { Screen, Wnd, Elm, Bitmap } internal AreaType Type; readonly bool _hasRect, _hasCoord; internal wnd W; internal elm E; internal Bitmap B; RECT _r; readonly Coord _cLeft, _cTop, _cRight, _cBottom; IFArea(AreaType t) { Type = t; } /// Specifies a window or control and a rectangle in its client area. public IFArea(wnd w, RECT r) { Type = AreaType.Wnd; W = w; _r = r; _hasRect = true; } /// Specifies a UI element and a rectangle in it. public IFArea(elm e, RECT r) { Type = AreaType.Elm; E = e; _r = r; _hasRect = true; } //rejected. Rare etc. //public IFArea(Bitmap b, RECT r) { ... } /// /// Specifies a window or control and a rectangle in its client area. /// The parameters are of type, therefore can be easily specified reverse and fractional coordinates, like ^10 and .5f. Use ^0 for right or bottom edge. /// public IFArea(wnd w, Coord left, Coord top, Coord right, Coord bottom) { Type = AreaType.Wnd; W = w; _cLeft = left; _cTop = top; _cRight = right; _cBottom = bottom; _hasRect = _hasCoord = true; } /// /// Specifies a UI element and a rectangle in it. /// The parameters are of type, therefore can be easily specified reverse and fractional coordinates, like ^10 and .5f. Use ^0 for right or bottom edge. /// public IFArea(elm e, Coord left, Coord top, Coord right, Coord bottom) { Type = AreaType.Elm; E = e; _cLeft = left; _cTop = top; _cRight = right; _cBottom = bottom; _hasRect = _hasCoord = true; } public static implicit operator IFArea(wnd w) => new(AreaType.Wnd) { W = w }; public static implicit operator IFArea(elm e) => new(AreaType.Elm) { E = e }; public static implicit operator IFArea(Bitmap b) => new(AreaType.Bitmap) { B = b }; public static implicit operator IFArea(RECT r) => new(AreaType.Screen) { _r = r }; internal void Before_(bool windowPixels) { if (windowPixels && Type is AreaType.Screen or AreaType.Bitmap) throw new ArgumentException("Invalid flags for this area type"); switch (Type) { case AreaType.Wnd: W.ThrowIfInvalid(); break; case AreaType.Elm: if (E == null) throw new ArgumentNullException("area elm"); W = E.WndContainer; goto case AreaType.Wnd; case AreaType.Bitmap: if (B == null) throw new ArgumentNullException("area Bitmap"); break; } } internal bool GetRect_(out RECT r, out POINT resultOffset, IFFlags flags) { r = default; resultOffset = default; if (!W.Is0) { if (!W.IsVisible || W.IsMinimized) return false; if (!flags.HasAny(IFFlags.WindowDC | IFFlags.PrintWindow) && W.IsCloaked) return false; } //Get area rectangle. bool failed = false; switch (Type) { case AreaType.Wnd: failed = !W.GetClientRect(out r); break; case AreaType.Elm: failed = !E.GetRect(out r, W); break; case AreaType.Bitmap: r = new RECT(0, 0, B.Width, B.Height); break; default: //Screen r = _r; if (!screen.isInAnyScreen(r)) r = default; resultOffset.x = r.left; resultOffset.y = r.top; break; } if (failed) { W.ThrowIfInvalid(); throw new AuException("*get rectangle"); } //r is the area from where to get pixels. If Wnd or Elm, it is relative to W client area. //Intermediate results will be relative to r. Then will be added resultOffset if need. if (_hasRect) { RECT rr; if (_hasCoord) { RECT rc = new(0, 0, r.Width, r.Height); POINT p1 = Coord.NormalizeInRect(_cLeft, _cTop, rc), p2 = Coord.NormalizeInRect(_cRight, _cBottom, rc); rr = RECT.FromLTRB(p1.x, p1.y, p2.x, p2.y); } else { rr = _r; } resultOffset.x = rr.left; resultOffset.y = rr.top; rr.Offset(r.left, r.top); r.Intersect(rr); } if (Type == AreaType.Elm) { //adjust r and resultOffset, // because object rectangle may be bigger than client area (eg WINDOW object) // or its part is not in client area (eg scrolled web page). // If not adjusted, then may capture part of parent or sibling controls or even other windows... // Never mind: should also adjust control rectangle in ancestors in the same way. // This is not so important because usually whole control is visible (resized, not clipped). int x = r.left, y = r.top; W.GetClientRect(out var rw); r.Intersect(rw); x -= r.left; y -= r.top; resultOffset.x -= x; resultOffset.y -= y; } return !r.NoArea; //never mind: if WaitChanged and this is the first time, immediately returns 'changed' } /// /// Calls GetRect_, and CaptureScreen.Image if need. /// /// false if GetRect_ returns false (empty rectangle). internal bool GetOcrData_(OcrFlags flags, out Bitmap b, out POINT resultOffset) { if (Type == AreaType.Bitmap) { b = B; resultOffset = default; return true; } if (!GetRect_(out RECT r, out resultOffset, (IFFlags)flags)) { b = null; return false; } b = Type == AreaType.Screen ? CaptureScreen.Image(r) : CaptureScreen.Image(W, r, flags.ToCIFlags_()); return true; } /// /// If scale!=0, returns scale. Else returns 1...2 depending on engine and area DPI. /// internal double GetOcrScale_(double scale, IOcrEngine engine) { if (scale != 0) return scale; if (engine.DpiScale) { int dpi = Type switch { AreaType.Wnd or AreaType.Elm => Dpi.OfWindow(W), AreaType.Screen => screen.of(_r).Dpi, _ => 200 }; if (dpi < 192) return 192d / dpi; } return 1d; } } /// /// Image(s) or color(s) for and similar functions. /// /// /// Has implicit conversions from: /// - string - path of .png or .bmp file. If not full path, uses . /// - string that starts with "resources/" or has prefix "resource:" - resource name; see . /// - string with prefix "image:" - Base64 encoded .png image.\ /// Can be created with tool Find image or color in window or with function Au.Controls.KImageUtil.ImageToString (in Au.Controls.dll). /// - , int or uint in 0xRRGGBB color format, - color. Alpha isn't used. /// - - image object. /// - IFImage[] - multiple images or/and colors. Action - find any. To create a different action can be used callback function (parameter also). /// /// Icons are not supported directly; you can use to get icon and convert to bitmap. /// public struct IFImage { readonly object _o; IFImage(object o) { _o = o; } public static implicit operator IFImage(string pathEtc) => new(pathEtc); public static implicit operator IFImage(Bitmap image) => new(image); public static implicit operator IFImage(ColorInt color) => new(color); public static implicit operator IFImage(int color) => new((ColorInt)color); public static implicit operator IFImage(uint color) => new((ColorInt)color); public static implicit operator IFImage(Color color) => new((ColorInt)color); public static implicit operator IFImage(System.Windows.Media.Color color) => new((ColorInt)color); //public static implicit operator IFImage(IEnumerable list) => new(list); //error: cannot convert from interfaces public static implicit operator IFImage(IFImage[] list) => new(list); //public static implicit operator IFImage(List list) => new(list); //rare, can use ToArray() /// /// Gets the raw value stored in this variable. Can be string, , , [], null. /// public object Value => _o; } #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member /// /// Flags for and similar functions. /// [Flags] public enum IFFlags { /// /// Get pixels from the device context (DC) of the window client area, not from screen DC. Usually much faster. /// Can get pixels from window parts that are covered by other windows or offscreen. But not from hidden and minimized windows. /// Does not work on Windows 7 if Aero theme is turned off. Then this flag is ignored. /// Cannot find images in some windows (including Windows Store apps), and in some window parts (glass). All pixels captured from these windows/parts are black. /// If the window is partially or completely transparent, the image must be captured from its non-transparent version. /// If the window is DPI-scaled, the image must be captured from its non-scaled version. However if used a limiting rectangle, it must contain scaled coordinates. /// WindowDC = 1, /// /// Use API PrintWindow to get window pixels. /// Like WindowDC, works with background windows, etc. Differences: ///
• On Windows 8.1 and later works with windows where WindowDC doesn't. ///
• Works without Aero theme too. ///
• Slower. ///
• Some windows flicker. ///
• From some controls randomly gets partial image (API bug). ///
• Does not work with windows of higher [](xref:uac) integrity level (throws exception). ///
• Unreliable with DPI-scaled windows; the window image slightly changes when resizing etc. ///
PrintWindow = 2, //rejected, makes no sense: ///
• If used together with flag WindowDC, calls PrintWindow without flag PW_RENDERFULLCONTENT. Faster, less flickering, no bugs, but has the WindowDC problem: can't get pixels from some windows or window parts. ///// ///// Use DWM thumbnail API to get window pixels. ///// Like WindowDC, works with background windows, etc. Differences: /////
• Works with windows where WindowDC doesn't. /////
• Requires Windows 10 or later. Exception if used on older OS. /////
• Slower. /////
• May not work with some windows (rare). /////
//WindowDwm = 4, //note: the above values must be the same in CIFlags, CIUFlags, IFFlags, OcrFlags. /// /// This flag can make the function faster when image is a list of images. To search for each image, the function will use instead of for. For example, if the CPU has 4 cores (8 threads), can search for max 8 images simultaneously. However it does not mean it will be 8 times faster. Can be max 2 or 3 times faster, depending on the number of images, flag WindowDC, diff, also, CPU, RAM, area size, finds or not, image position, etc. Can be even slower. To measure speed, use . /// If used also callback function, it runs in any thread and any order, but one at a time (inside lock() { }). /// Parallel = 0x100 //rejected: this was used in QM2. Now can use png alpha instead. ///// ///// Use the top-left pixel color of the image as transparent color (don't compare pixels that have this color). ///// //MakeTransparent = , } /// /// Used with and . Its callback function (parameter also) can return one of these values. /// public enum IFAlso { /// /// Stop searching. /// Let the main function return current result. /// OkReturn, /// /// Find more instances of current image. If used list of images, also search for other images. /// Then let the main function return current result. /// OkFindMore, /// /// Find more instances of current image. When used list of images, don't search for other images. /// Then let the main function return current result. /// OkFindMoreOfThis, /// /// If used list of images, search for other images. Don't search for more instances of current image. /// Then let the main function return current result. /// OkFindMoreOfList, /// /// Stop searching. /// Let the main function return null or throw exception or continue waiting. But if a OkFindX value used previously, return that result. /// NotFound, /// /// Find more instances of current image. If used list of images, also search for other images. /// If not found, let the main function return null or throw exception or continue waiting; but if a OkFindX value used previously, return that result. /// FindOther, /// /// Find more instances of current image. When used list of images, don't search for other images. /// If not found, let the main function return null or throw exception or continue waiting; but if a OkFindX value used previously, return that result. /// FindOtherOfThis, /// /// If used list of images, search for other images. Don't search for more instances of current image. /// If not found, let the main function return null or throw exception or continue waiting; but if a OkFindX value used previously, return that result. /// FindOtherOfList, } ================================================ FILE: Au/resources/global2.cs ================================================ //This file is used by several projects: Au, Au.Controls, Au.Editor. global using Au; global using Au.Types; global using Au.More; global using System; global using System.Collections.Generic; global using System.Collections.Concurrent; global using System.Linq; global using System.Text; global using System.Diagnostics; global using System.Runtime.CompilerServices; global using System.Runtime.InteropServices; global using System.IO; global using System.Threading; global using System.Threading.Tasks; global using System.Reflection; global using System.Globalization; global using SystemInformation = System.Windows.Forms.SystemInformation; global using RStr = System.ReadOnlySpan; global using RByte = System.ReadOnlySpan; global using System.ComponentModel; global using IEnumerable = System.Collections.IEnumerable; global using IEnumerator = System.Collections.IEnumerator; [module: DefaultCharSet(CharSet.Unicode)] [assembly: ComVisible(false)] [assembly: AssemblyCompany("Gintaras Didžgalvis")] [assembly: AssemblyProduct("LibreAutomate")] [assembly: AssemblyCopyright("Copyright 2020-2026 Gintaras Didžgalvis")] [assembly: AssemblyCulture("")] [assembly: AssemblyVersion(Au_.Version)] #if AU namespace Au.More; /// [EditorBrowsable(EditorBrowsableState.Never)] public class Au_ { /// public const string Version = "1.15.0"; } #endif ================================================ FILE: Au/wnd/WProp.cs ================================================ namespace Au.Types { /// /// Sets, gets, removes and lists window properties using API SetProp and co. /// public struct WProp { readonly wnd _w; internal WProp(wnd w) => _w = w; /// /// Gets a window property. /// Calls API GetProp and returns its return value. /// /// Property name. /// Supports . public nint this[string name] => Api.GetProp(_w, name); /// /// Gets a window property. /// Calls API GetProp and returns its return value. /// /// Property name atom in the global atom table. /// /// This overload uses atom instead of string. It's about 3 times faster. See API GlobalAddAtom, GlobalDeleteAtom. /// public nint this[ushort atom] => Api.GetProp(_w, atom); /// /// Sets a window property. /// Calls API SetProp and returns its return value. /// /// Property name. /// Property value. /// /// Supports . /// /// Later call to remove the property. If you use many unique property names and don't remove the properties, the property name strings can fill the global atom table which is of a fixed size (about 48000) and which is used by all processes for various purposes. /// public bool Set(string name, nint value) { return Api.SetProp(_w, name, value); } /// /// Sets a window property. /// Calls API SetProp and returns its return value. /// /// Property name atom in the global atom table. /// Property value. /// /// This overload uses atom instead of string. It's about 3 times faster. See API GlobalAddAtom, GlobalDeleteAtom. /// public bool Set(ushort atom, nint value) { return Api.SetProp(_w, atom, value); } /// /// Removes a window property. /// Calls API RemoveProp and returns its return value. /// /// Property name. Other overload allows to use global atom instead, which is faster. /// Supports . public nint Remove(string name) { return Api.RemoveProp(_w, name); } /// /// Removes a window property. /// Calls API RemoveProp and returns its return value. /// /// Property name atom in the global atom table. public nint Remove(ushort atom) { return Api.RemoveProp(_w, atom); } /// /// Gets list of window properties. /// Uses API EnumPropsEx. /// /// 0-length list if failed. Fails if invalid window or access denied ([](xref:uac)). Supports . public Dictionary GetList() { var a = new Dictionary(); Api.EnumPropsEx(_w, (w, name, data, p) => { string s; if ((long)name < 0x10000) s = "#" + (int)name; else s = Marshal.PtrToStringUni(name); a.Add(s, data); return true; }, default); return a; } /// /// Calls and converts to string. /// public override string ToString() { return string.Join("\r\n", GetList()); } } } ================================================ FILE: Au/wnd/WTaskbarButton.cs ================================================ namespace Au.Types; /// /// Taskbar button flash, progress, add/delete. /// public unsafe class WTaskbarButton { readonly wnd _w; internal WTaskbarButton(wnd w) => _w = w; /// /// Starts or stops flashing the taskbar button of this window. /// /// The number of times to flash. If 0, stops flashing. public void Flash(int count) { //const uint FLASHW_STOP = 0; //const uint FLASHW_CAPTION = 0x00000001; const uint FLASHW_TRAY = 0x00000002; //const uint FLASHW_ALL = FLASHW_CAPTION | FLASHW_TRAY; //const uint FLASHW_TIMER = 0x00000004; //const uint FLASHW_TIMERNOFG = 0x0000000C; var fi = new Api.FLASHWINFO { cbSize = sizeof(Api.FLASHWINFO), hwnd = _w }; if (count > 0) { fi.uCount = count; //fi.dwTimeout = (uint)periodMS; //not useful fi.dwFlags = FLASHW_TRAY; } Api.FlashWindowEx(ref fi); //tested. FlashWindow is easier but does not work for taskbar button, only for title bar when no taskbar button. } /// /// Sets the state of the progress indicator displayed on the taskbar button of this window. /// Calls ITaskbarList3.SetProgressState. /// /// Progress indicator state and color. public void SetProgressState(WTBProgressState state) { _TL?.SetProgressState(_w, state); } /// /// Sets the value of the progress indicator displayed on the taskbar button of this window. /// Calls ITaskbarList3.SetProgressValue. /// /// Progress indicator value, 0 to progressTotal. /// Max progress indicator value. public void SetProgressValue(int progressValue, int progressTotal = 100) { _TL?.SetProgressValue(_w, progressValue, progressTotal); } /// /// Adds taskbar button of this window. /// Calls ITaskbarList.AddTab. /// public void Add() { _TL?.AddTab(_w); //tested: always returns 0, even if w is 0. Did not test other ITaskbarList3 methods. } /// /// Deletes taskbar button of this window. /// Calls ITaskbarList.DeleteTab. /// public void Delete() { _TL?.DeleteTab(_w); } static ITaskbarList3 _TL { get { var r = (ITaskbarList3)new TaskbarList(); if (0 != r.HrInit()) return null; return r; } } [ComImport, Guid("56fdf344-fd6d-11d0-958a-006097c9a090"), ClassInterface(ClassInterfaceType.None)] class TaskbarList { } [ComImport, Guid("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] interface ITaskbarList3 { // ITaskbarList [PreserveSig] int HrInit(); [PreserveSig] int AddTab(wnd hwnd); [PreserveSig] int DeleteTab(wnd hwnd); [PreserveSig] int ActivateTab(wnd hwnd); [PreserveSig] int SetActiveAlt(wnd hwnd); // ITaskbarList2 [PreserveSig] int MarkFullscreenWindow(wnd hwnd, bool fFullscreen); // ITaskbarList3 [PreserveSig] int SetProgressValue(wnd hwnd, long ullCompleted, long ullTotal); [PreserveSig] int SetProgressState(wnd hwnd, WTBProgressState state); [PreserveSig] int RegisterTab(wnd hwndTab, wnd hwndMDI); [PreserveSig] int UnregisterTab(wnd hwndTab); [PreserveSig] int SetTabOrder(wnd hwndTab, wnd hwndInsertBefore); [PreserveSig] int SetTabActive(wnd hwndTab, wnd hwndMDI, uint dwReserved); [PreserveSig] int ThumbBarAddButtons(wnd hwnd, uint cButtons, IntPtr pButton); //LPTHUMBBUTTON [PreserveSig] int ThumbBarUpdateButtons(wnd hwnd, uint cButtons, IntPtr pButton); //LPTHUMBBUTTON [PreserveSig] int ThumbBarSetImageList(wnd hwnd, IntPtr himl); [PreserveSig] int SetOverlayIcon(wnd hwnd, IntPtr hIcon, string pszDescription); [PreserveSig] int SetThumbnailTooltip(wnd hwnd, string pszTip); [PreserveSig] int SetThumbnailClip(wnd hwnd, ref RECT prcClip); } } /// /// Used by . /// public enum WTBProgressState { #pragma warning disable 1591 //XML doc NoProgress = 0, Indeterminate = 0x1, Normal = 0x2, Error = 0x4, Paused = 0x8 #pragma warning restore 1591 //XML doc } ================================================ FILE: Au/wnd/WndCopyData.cs ================================================ namespace Au.More { /// /// Send/receive data to/from other process using message WM_COPYDATA. /// /// /// This struct is COPYDATASTRUCT. /// By default [](xref:uac) blocks messages sent from processes of lower integrity level. Call if need. /// /// /// public unsafe struct WndCopyData { //COPYDATASTRUCT fields nint _dwData; int _cbData; byte* _lpData; #region receive /// /// Initializes this variable from lParam of a received WM_COPYDATA message. /// Then you can call functions of this variable to get data in managed format. /// /// lParam of a WM_COPYDATA message received in a window procedure. It is COPYDATASTRUCT pointer. public WndCopyData(nint lParam) { var p = (WndCopyData*)lParam; _dwData = p->_dwData; _cbData = p->_cbData; _lpData = p->_lpData; } /// /// Data id. It is COPYDATASTRUCT.dwData. /// public int DataId { get => (int)_dwData; set => _dwData = value; } /// /// Unmanaged data pointer. It is COPYDATASTRUCT.lpData. /// public byte* RawData { get => _lpData; set => _lpData = value; } /// /// Unmanaged data size. It is COPYDATASTRUCT.cbData. /// public int RawDataSize { get => _cbData; set => _cbData = value; } /// /// Gets received data as string. /// public string GetString() { return new string((char*)_lpData, 0, _cbData / 2); } /// /// Gets received data as byte[]. /// public byte[] GetBytes() { var a = new byte[_cbData]; Marshal.Copy((IntPtr)_lpData, a, 0, a.Length); return a; } /// /// Calls API ChangeWindowMessageFilter(WM_COPYDATA). Then windows of this process can receive this message from lower [](xref:uac) integrity level processes. /// public static void EnableReceivingWM_COPYDATA() { Api.ChangeWindowMessageFilter(Api.WM_COPYDATA, 1); } #endregion #region send /// /// Sends string or other data to a window of any process. Uses API SendMessage WM_COPYDATA. /// /// Type of data elements. For example, char for string, byte for byte[]. /// The window. /// Data id. It is COPYDATASTRUCT.dwData. /// Data. For example string or byte[]. String can contain '\0' characters. /// Can be any value. Optional. /// SendMessage's return value. public static unsafe nint Send(wnd w, int dataId, ReadOnlySpan data, nint wParam = 0) where T : unmanaged { fixed (T* p = data) { var c = new WndCopyData { _dwData = dataId, _cbData = data.Length * sizeof(T), _lpData = (byte*)p }; return w.Send(Api.WM_COPYDATA, wParam, &c); } } /// /// Type of callback function. /// /// Received data buffer. The callback function can convert it to array, string, etc. public delegate void ResultReader(ReadOnlySpan span) where TReceive : unmanaged; //compiler error if Action>. //could instead use System.Buffers.ReadOnlySpanAction, but then need TState, which is difficult to use for return, and nobody would use, and would not make faster etc. static readonly Lazy s_mutex = new(Api.CreateMutex(null, false, "Au-mutex-WndUtil.Data")); //tested: don't need Api.SECURITY_ATTRIBUTES.ForLowIL /// /// Sends string or other data to a window of any process. Uses API SendMessage WM_COPYDATA. /// Receives string or other data returned by that window with . /// /// Type of data elements. For example, char for string, byte for byte[] /// Type of received data elements. For example, char for string, byte for byte[]. /// The window. /// Data id. It is COPYDATASTRUCT.dwData. /// Data to send. For example string or byte[]. String can contain '\0' characters. /// Callback function that can convert the received data to desired format. /// false if failed. public static unsafe bool SendReceive(wnd w, int dataId, ReadOnlySpan send, ResultReader receive) where TSend : unmanaged where TReceive : unmanaged { var mutex = s_mutex.Value; if (Api.WaitForSingleObject(mutex, -1) is not (0 or Api.WAIT_ABANDONED_0)) return false; try { int len = (int)Send(w, dataId, send, Api.GetCurrentProcessId()); if (len == 0) return false; var sm = SharedMemory_.ReturnDataPtr; if (len > 0) { //shared memory if (len <= SharedMemory_.ReturnDataSize) { receive(new ReadOnlySpan((TReceive*)sm, len / sizeof(TReceive))); } else { using var m2 = SharedMemory_.Mapping.CreateOrOpen(new((char*)sm), len); receive(new ReadOnlySpan((TReceive*)m2.Mem, len / sizeof(TReceive))); } } else { //process memory var pm = (void*)*(long*)sm; receive(new ReadOnlySpan((TReceive*)pm, -len / sizeof(TReceive))); bool ok = Api.VirtualFree(pm); Debug_.PrintIf(!ok, "VirtualFree"); } return true; } finally { Api.ReleaseMutex(mutex); } } /// /// Calls and gets the received data as byte[]. /// /// The received data. /// public static bool SendReceive(wnd w, int dataId, ReadOnlySpan send, out byte[] received) where TSend : unmanaged { byte[] r = null; bool R = SendReceive(w, dataId, send, span => r = span.ToArray()); received = r; return R; } /// /// Calls and gets the received string. /// /// The received data. /// public static bool SendReceive(wnd w, int dataId, ReadOnlySpan send, out string received) where TSend : unmanaged { string r = null; bool R = SendReceive(w, dataId, send, span => r = span.ToString()); received = r; return R; } /// /// Returns data to . /// /// /// /// wParam of the received WM_COPYDATA message. Important, pass unchanged. /// Your window procedure must return this value. public static unsafe int Return(void* data, int length, nint wParam) { var sm = SharedMemory_.ReturnDataPtr; //use shared memory of this library. Max 1 MB. if (length <= SharedMemory_.ReturnDataSize) { MemoryUtil.Copy(data, sm, length); return length; } //allocate memory in caller process using var pm = new ProcessMemory((int)wParam, length, noException: true); if (pm.ProcessHandle != default) { //fails if that process has higher UAC IL. Rare. pm.Write(data, length); *(long*)sm = (long)pm.Mem; pm.MemAllocated = default; return -length; } //allocate new shared memory try { var smname = "Au-memory-" + Guid.NewGuid().ToString(); fixed (char* p = smname) MemoryUtil.Copy(p, sm, smname.Length * 2 + 2); var m2 = SharedMemory_.Mapping.CreateOrOpen(smname, length); MemoryUtil.Copy(data, m2.Mem, length); Task.Run(() => { //wait until caller returns and then close the shared memory in this process var mutex = s_mutex.Value; if (Api.WaitForSingleObject(mutex, -1) is not (0 or Api.WAIT_ABANDONED_0)) { Debug_.Print("WaitForSingleObject"); return; } Api.ReleaseMutex(mutex); m2.Dispose(); }); return length; } catch { return 0; } //speed when size 1 MB and hot CPU: // shared memory: 1000 mcs // process memory: 1500 mcs // shared memory 2: 2500 mcs } /// /// Returns string or other data to . /// /// Type of data elements. For example, char for string, byte for byte[] /// /// wParam of the received WM_COPYDATA message. Important, pass unchanged. /// Your window procedure must return this value. public static unsafe int Return(ReadOnlySpan data, nint wParam) where T : unmanaged { fixed (T* f = data) return Return(f, data.Length * sizeof(T), wParam); } //rejected. Don't need too many not important overloads. Good: in most cases data size is 2 times smaller. Same: speed. //[SkipLocalsInit] //public static unsafe int ReturnStringUtf8_(RStr data, nint wParam) { // var e = Encoding.UTF8; // using var b = new FastBuffer(e.GetByteCount(data)); // int len = e.GetBytes(data, new Span(b.p, b.n)); // return ReturnData_(b.p, len, wParam); //} #endregion } } ================================================ FILE: Au/wnd/WndSavedRect.cs ================================================ //TODO3: it seems makes WPF window visible too early. //TODO3: if window not resizable, restore only position, not size. namespace Au.More; /// /// Helps to save and restore window rectangle and state. Ensures in screen, per-monitor-DPI-aware, etc. /// /// /// WPF window created with . /// Registry.SetValue(c_rkey, c_rvalue, s1)); /// /// //the same /// //b.WinSaved(Registry.GetValue(c_rkey, c_rvalue, null) as string, s1 => Registry.SetValue(c_rkey, c_rvalue, s1)); /// /// if (!b.ShowDialog()) return; /// ]]> /// public struct WndSavedRect { /// /// Window rectangle in normal state (not maximized/minimized), as retrieved by API GetWindowPlacement. /// public RECT RawRect { get => _r; set => _r = value; } RECT _r; /// /// . /// public int Dpi { get; set; } /// /// The window should be maximized. /// public bool Maximize { get; set; } /// /// . If false, may have an offset that depends on work area. /// public bool IsToolWindow { get; set; } /// /// Converts this object to string for saving. /// The string is very simple, like "1 2 3 4 5 6". /// public override string ToString() { return $"{_r.left} {_r.top} {_r.Width} {_r.Height} {Dpi} {(Maximize ? 1 : 0) | (IsToolWindow ? 2 : 0)}"; } /// /// Creates from string created by . /// /// false if the string is null or invalid. /// String created by . /// Result. public static bool FromString(string saved, out WndSavedRect x) { x = default; if (saved == null) return false; var a = new int[6]; for (int i = 0, j = 0; i < a.Length; i++) if (!saved.ToInt(out a[i], j, out j)) return false; x._r = (a[0], a[1], a[2], a[3]); x.Dpi = a[4]; var flags = a[5]; x.Maximize = 0 != (flags & 1); x.IsToolWindow = 0 != (flags & 2); return true; } /// /// Gets window rectangle and state for saving. Usually called when closing the window. /// See also . /// /// Failed to get rectangle, probably invalid window handle. public WndSavedRect(wnd w) { if (!w.GetWindowPlacement_(out var p, false)) w.ThrowUseNative(); _r = p.rcNormalPosition; Dpi = More.Dpi.OfWindow(w); Maximize = p.showCmd == Api.SW_SHOWMAXIMIZED || (p.showCmd == Api.SW_SHOWMINIMIZED && 0 != (p.flags & Api.WPF_RESTORETOMAXIMIZED)); IsToolWindow = w.IsToolWindow; } /// /// Gets window rectangle and state for saving. Usually called when closing the window. /// See also . /// /// Failed to get rectangle, probably invalid window handle. public WndSavedRect(System.Windows.Window w) : this(w.Hwnd()) { } /// /// Gets window rectangle and state for saving. Usually called when closing the window. /// See also . /// /// Failed to get rectangle, probably invalid window handle. public WndSavedRect(System.Windows.Forms.Form form) : this(form.Hwnd()) { } /// /// Gets real rectangle for restoring saved window rectangle. /// /// /// It is recommended to call this before creating window, and create window with the returned rectangle. Also set maximized state if . /// If it is not possible, can be called later, for example when window is created but still invisible. However then possible various problems, for example may need to set window rectangle two times, because the window may be for example DPI-scaled when moving to another screen etc. /// /// This function ensures the window is in screen, ensures correct size when screen DPI changed, etc. /// public RECT NormalizeRect() { var r = _r; var scr = screen.of(r); int dpi = scr.Dpi; if (dpi != this.Dpi) { r.Width = Math2.MulDiv(r.Width, dpi, this.Dpi); r.Height = Math2.MulDiv(r.Height, dpi, this.Dpi); //don't change xy. Anyway we cannot cover all cases, eg changed DPI of another screen that could affect xy of the window in this screen. } if (!IsToolWindow) { var v = scr.Info; r.Offset(v.workArea.left - v.rect.left, v.workArea.top - v.rect.top); } r.EnsureInScreen(scr, workArea: !IsToolWindow); //TODO3: use simple rect adjust. Or add EnsureInRect. return r; } /// /// Calls . If it returns true, calls , , maximizes if need and returns true. /// Call this function before showing window. /// /// /// String created by . /// If not null, called when closing the window. Receives string for saving. Can save it in registry, file, anywhere. /// Window is loaded. public static bool Restore(System.Windows.Window w, string saved, Action save = null) { if (w.IsLoaded) throw new InvalidOperationException("Window is loaded."); bool ret = FromString(saved, out var v); if (ret) { var r = v.NormalizeRect(); if (v.Maximize) w.WindowState = System.Windows.WindowState.Maximized; w.SetRect(r); } if (save != null) { w.Closing += (_, _) => { if (w.IsLoaded) save(new WndSavedRect(w).ToString()); }; } return ret; } /// /// Calls . If it returns true, sets form bounds = , maximizes if need, sets StartPosition = Manual, and returns true. /// Call this function before showing window. /// /// /// String created by . /// If not null, called when closing the window. Receives string for saving. Can save it in registry, file, anywhere. public static bool Restore(System.Windows.Forms.Form form, string saved, Action save = null) { bool ret = FromString(saved, out var v); if (ret) { form.StartPosition = System.Windows.Forms.FormStartPosition.Manual; form.Bounds = v.NormalizeRect(); if (v.Maximize) form.WindowState = System.Windows.Forms.FormWindowState.Maximized; } if (save != null) { form.FormClosing += (_, _) => { if (form.IsHandleCreated) save(new WndSavedRect(form).ToString()); }; } return ret; } //probably not useful. Unfinished. Or move to wnd. ///// ///// Calls . If it returns true, sets w rectangle = , maximizes if need, and returns true. ///// ///// ///// String created by . ///// If need to maximize and the window already is maximized, set the restored window rectangle too. //public static unsafe bool Restore(wnd w, string saved, bool allStates = false) { // w.ThrowIfInvalid(); // bool ret = FromString(saved, out var v); // if (ret) { // //var p = new Api.WINDOWPLACEMENT { // // rcNormalPosition = v.NormalizeRect(), // // showCmd = v.Maximize ? Api.SW_SHOWMAXIMIZED : Api.SW_RESTORE // //}; // //w.SetWindowPlacement_(ref p, false, "Failed to restore window position"); // if (!allStates && v.Maximize && w.IsMaximized) return true; // if (w.IsMaximized) w.ShowNotMinMax(true); // if(!w.MoveL(v.NormalizeRect())) w.ThrowUseNative("Failed to restore window position"); // if (v.Maximize) w.ShowMaximized(true); // } // return ret; //} } ================================================ FILE: Au/wnd/WndUtil.cs ================================================ namespace Au.More { /// /// Miscellaneous window-related functions. Rarely used in automation scripts. /// public static class WndUtil { //public void ShowAnimate(bool show) //{ // //Don't add wnd function, because: // //Rarely used. // //Api.AnimateWindow() works only with windows of current thread. // //Only programmers would need it, and they can call the API directly. //} /// /// Registers new window class in this process. /// /// Class name. /// /// Delegate of a window procedure. See Window Procedures. /// /// Use null when you need a different delegate (method or target object) for each window instance; create windows with or . /// If not null, it must be a static named method; create windows with any other function, including API CreateWindowEx. /// /// /// Can be used to specify API WNDCLASSEX fields. /// To set cursor use field mCursor (standard cursor) or hCursor (native handle of a custom cursor). /// If null, this function sets arrow cursor and style CS_VREDRAW | CS_HREDRAW. /// /// wndProc is an instance method. Must be static method or null. If need instance method, use null here and pass wndProc to . /// The class already registered with this function and different wndProc (another method or another target object). /// Failed, for example if the class already exists and was registered not with this function. /// /// Calls API RegisterClassEx. /// The window class is registered until this process ends. Don't need to unregister. /// If called next time for the same window class, does nothing if wndProc is equal to the previous (or both null). Then ignores etc. Throws exception if different. /// Thread-safe. /// Protects the wndProc delegate from GC. /// public static unsafe void RegisterWindowClass(string className, WNDPROC wndProc = null, RWCEtc etc = null) { if (wndProc?.Target != null) throw new ArgumentException("wndProc must be static method or null"); //never mind: Target of static lambda != null. Could use code `&& !wndProc.Target.GetType().FullName.Ends("+<>c")`, but it's undocumented and may stop working in new .NET version. lock (s_classes) { if (s_classes.TryGetValue(className, out var wpPrev)) { if (wpPrev != wndProc) throw new InvalidOperationException("Window class already registered"); //another method or another target object return; } var x = new Api.WNDCLASSEX(etc); fixed (char* pCN = className) { x.lpszClassName = pCN; if (wndProc != null) { x.lpfnWndProc = Marshal.GetFunctionPointerForDelegate(wndProc); } else { x.lpfnWndProc = s_cwProcFP; } x.style |= Api.CS_GLOBALCLASS; if (0 == Api.RegisterClassEx(x)) throw new Win32Exception(); //note: we don't return atom because: 1. Rarely used. 2. If assigned to an unused field, compiler may remove the function call. s_classes.Add(className, wndProc); } } } internal static bool IsClassRegistered_(string name, out WNDPROC wndProc) { lock (s_classes) { return s_classes.TryGetValue(name, out wndProc); } } static Dictionary s_classes = new(StringComparer.OrdinalIgnoreCase); //allows to find registered classes and protects their wndProc delegates from GC [ThreadStatic] static Dictionary t_windows; //allows to dispatch messages and protects wndProc delegates of windows created in this thread from GC static nint _CWProc(wnd w, int msg, nint wParam, nint lParam) { //PrintMsg(w, msg, wParam, lParam); if (t_cwUnsafe) { t_cwUnsafe = false; var wndProc = t_cwProc; t_cwProc = null; Api.SetWindowLongPtr(w, GWL.WNDPROC, Marshal.GetFunctionPointerForDelegate(wndProc)); //print.it("subclassed", w); return wndProc(w, msg, wParam, lParam); } else { var a = t_windows; if (a == null || !a.TryGetValue(w, out var wndProc)) { wndProc = t_cwProc; if (wndProc == null) { //print.it("DefWindowProc", w); return Api.DefWindowProc(w, msg, wParam, lParam); //creating not with our CreateWindow(wndProc, ...) } a[w] = wndProc; //print.it("added", a.Count, w); t_cwProc = null; } var R = wndProc(w, msg, wParam, lParam); if (msg == Api.WM_NCDESTROY) { a.Remove(w); //print.it("removed", a.Count, w); } return R; } } static WNDPROC s_cwProc; //GC static IntPtr s_cwProcFP = Marshal.GetFunctionPointerForDelegate(s_cwProc = _CWProc); [ThreadStatic] static WNDPROC t_cwProc; [ThreadStatic] static bool t_cwUnsafe; /// /// Creates native/unmanaged window (API CreateWindowEx) and sets its window procedure. /// /// Window procedure. /// /// Protect wndProc from GC (garbage collector) until the window is destroyed (message WM_NCDESTROY received or thread ended). /// IMPORTANT: In some cases it may prevent destroying the window until thread ends, and it can be a big memory leak. For example WPF then does not destroy HwndHost-ed controls. Then let keepAlive=false and manually manage wndProc lifetime, for example keep it as a field of the wrapper class. /// /// Failed to create window. Unlikely. /// /// If the class was registered with with null wndProc, the wndProc function will receive all messages. Else will not receive messages sent before CreateWindowEx returns (WM_CREATE etc). /// /// To destroy the window can be used any function, including API DestroyWindow, , , API WM_CLOSE. /// public static wnd CreateWindow(WNDPROC wndProc, bool keepAlive, string className, string name = null, WS style = 0, WSE exStyle = 0, int x = 0, int y = 0, int width = 0, int height = 0, wnd parent = default, nint controlId = 0, IntPtr hInstance = default, nint param = 0) { Not_.Null(wndProc, className); t_windows ??= new(); wnd w; if (IsClassRegistered_(className, out var wp) && wp == null) { //if keepAlive, need to cubclass the new window, else add hwnd+wndProc to t_windows. // But not after CreateWindowEx, because wndProc must receive all messages. // Let _CWProc do it on first message. t_cwProc = wndProc; t_cwUnsafe = !keepAlive; try { w = Api.CreateWindowEx(exStyle, className, name, style, x, y, width, height, parent, controlId, hInstance, param); } finally { t_cwProc = null; t_cwUnsafe = false; } //if CreateWindowEx failed and _CWProc not called if (w.Is0) throw new AuException(0); } else { w = Api.CreateWindowEx(exStyle, className, name, style, x, y, width, height, parent, controlId, hInstance, param); if (w.Is0) throw new AuException(0); if (keepAlive) { t_windows[w] = wndProc; Api.SetWindowLongPtr(w, GWL.WNDPROC, s_cwProcFP); } else { Api.SetWindowLongPtr(w, GWL.WNDPROC, Marshal.GetFunctionPointerForDelegate(wndProc)); } } return w; } /// /// Creates native/unmanaged window. /// /// Failed to create window. Unlikely. /// /// Calls API CreateWindowEx. /// To destroy the window can be used any function, including API DestroyWindow, , , API WM_CLOSE. /// /// public static wnd CreateWindow(string className, string name = null, WS style = 0, WSE exStyle = 0, int x = 0, int y = 0, int width = 0, int height = 0, wnd parent = default, nint controlId = 0, IntPtr hInstance = default, nint param = 0) { var w = Api.CreateWindowEx(exStyle, className, name, style, x, y, width, height, parent, controlId, hInstance, param); if (w.Is0) throw new AuException(0); return w; } /// /// Creates native/unmanaged message-only window. /// /// Window class name. Can be any existing class. /// Window name or null. /// Failed to create window. Unlikely. /// /// Styles: WS_POPUP, WS_EX_NOACTIVATE. /// To destroy the window can be used any function, including API DestroyWindow, , , API WM_CLOSE. /// public static wnd CreateMessageOnlyWindow(string className, string name = null) { return CreateWindow(className, name, WS.POPUP, WSE.NOACTIVATE, parent: SpecHWND.MESSAGE); //note: WS_EX_NOACTIVATE is important. } /// /// Creates native/unmanaged message-only window and sets its window procedure. /// /// /// Window class name. /// Window name or null. /// Failed to create window. Unlikely. /// /// Styles: WS_POPUP, WS_EX_NOACTIVATE. /// Calls with keepAlive=true. /// public static wnd CreateMessageOnlyWindow(WNDPROC wndProc, string className, string name = null) { return CreateWindow(wndProc, true, className, name, WS.POPUP, WSE.NOACTIVATE, parent: SpecHWND.MESSAGE); //note: WS_EX_NOACTIVATE is important. } /// /// Auto-registers window class "Au.DWP" with wndproc = DefWindowProc and creates hidden window. /// /// /// If not null, replaces window procedure (SetWindowLongPtr). The caller must protect the delegate from GC. /// Failed to create window. Unlikely. internal static wnd CreateWindowDWP_(bool messageOnly, WNDPROC wndProcUnsafe = null) { var cn = WindowClassDWP_; var w = messageOnly ? CreateMessageOnlyWindow(cn) : CreateWindow(cn); if (wndProcUnsafe != null) Api.SetWindowLongPtr(w, GWL.WNDPROC, Marshal.GetFunctionPointerForDelegate(wndProcUnsafe)); return w; } static int s_registeredDWP; const string c_wndClassDWP = "Au.DWP"; /// /// Auto-registers window class "Au.DWP" with wndproc = DefWindowProc and returns "Au.DWP". /// internal static unsafe string WindowClassDWP_ { get { if (0 == Interlocked.CompareExchange(ref s_registeredDWP, 1, 0)) { var x = new Api.WNDCLASSEX { cbSize = sizeof(Api.WNDCLASSEX), style = Api.CS_GLOBALCLASS }; fixed (char* pCN = c_wndClassDWP) { x.lpszClassName = pCN; x.lpfnWndProc = Api.GetProcAddress("user32.dll", "DefWindowProcW"); if (0 == Api.RegisterClassEx(x)) throw new Win32Exception(); } } return c_wndClassDWP; } } /// /// Replaces window procedure (SetWindowLongPtr). Returns previous window procedure. /// The caller must protect the delegate from GC. /// internal static IntPtr SubclassUnsafe_(wnd w, WNDPROC wndProc) { return Api.SetWindowLongPtr(w, GWL.WNDPROC, Marshal.GetFunctionPointerForDelegate(wndProc)); } /// /// Destroys a native window of this thread. /// Calls API DestroyWindow. /// /// false if failed. Supports . /// public static bool DestroyWindow(wnd w) { return Api.DestroyWindow(w); } /// /// Sets font. /// /// /// /// Native font handle. /// If default(IntPtr), sets font that is used by most windows and controls on this computer, usually Segoe UI 9, DPI-scaled for w screen. /// /// /// Sends WM_SETFONT message. /// public static void SetFont(wnd w, IntPtr font = default) { w.Send(Api.WM_SETFONT, font != default ? font : NativeFont_.RegularCached(Dpi.OfWindow(w)).Handle); } //rejected. Rarely used. Easy to send message. ///// ///// Gets native font handle. ///// Sends message API WM_GETFONT. ///// Does not copy the font; don't need to dispose. ///// Use this function only with windows of current process. ///// //public static IntPtr GetFont(wnd w) //{ // return w.Send(Api.WM_GETFONT); //} /// /// Gets window Windows Store app user model id, like "Microsoft.WindowsCalculator_8wekyb3d8bbwe!App". /// /// null if failed. On Windows 7 returns null unless getExePathIfNotWinStoreApp true. /// A top-level window. /// Prepend @"shell:AppsFolder\" (to run or get icon). /// Get program path if it is not a Windows Store app. /// /// Most Windows Store app windows have class name "Windows.UI.Core.CoreWindow" or "ApplicationFrameWindow". /// public static unsafe string GetWindowsStoreAppId(wnd w, bool prependShellAppsFolder = false, bool getExePathIfNotWinStoreApp = false) { string appId = null; if (osVersion.minWin8) { var cn = w.ClassName; if (osVersion.minWin10 && cn == "ApplicationFrameWindow") { var w2 = w.ChildFast(null, "Windows.UI.Core.CoreWindow"); if (!w2.Is0) { w = w2; cn = "Windows.UI.Core.CoreWindow"; } else { //probably minimized. Very slow, ~20 times slower than GetApplicationUserModelId. if (0 == Api.SHGetPropertyStoreForWindow(w, Api.IID_IPropertyStore, out Api.IPropertyStore ps)) { if (0 == ps.GetValue(Api.PKEY_AppUserModel_ID, out var v)) { if (v.vt == Api.VARENUM.VT_LPWSTR) appId = Marshal.PtrToStringUni(v.value); v.Dispose(); } Marshal.ReleaseComObject(ps); } } } //this code works with "Windows.UI.Core.CoreWindow" and WinUI 3 windows. Not with "ApplicationFrameWindow". if (appId == null) { using var p = Handle_.OpenProcess(w); if (!p.Is0) { int na = 1024; var b = stackalloc char[na]; if (0 == Api.GetApplicationUserModelId(p, ref na, b) && na > 1) appId = new(b, 0, na - 1); } } if (appId != null) { if (cn is not ("Windows.UI.Core.CoreWindow" or "ApplicationFrameWindow")) { //is it really a Store window? var s = w.ProgramPath; if (s != null && !s.Starts(folders.ProgramFiles + @"WindowsApps\", true)) { Debug_.Print(s); return getExePathIfNotWinStoreApp ? s : null; } } if (prependShellAppsFolder) appId = @"shell:AppsFolder\" + appId; return appId; } } return getExePathIfNotWinStoreApp ? w.ProgramPath : null; } /// /// Calls API GetClassLongPtr. /// /// /// Supports . /// For index can be used constants from . All values are the same in 32-bit and 64-bit process. /// In 32-bit process actually calls GetClassLong, because GetClassLongPtr is unavailable. /// public static nint GetClassLong(wnd w, int index) => Api.GetClassLongPtr(w, index); /// /// Changes the owner window. /// /// If fails, returns false; supports . /// /// A window that has an owner window is always on top of it. /// Don't call this for controls, they don't have an owner window. /// Fails for example if the owner's process has higher [](xref:uac) integrity level or is a Store app. /// /// public static bool SetOwnerWindow(wnd w, wnd owner) { Api.SetWindowLongPtr(w, GWL.HWNDPARENT, (nint)owner); if (w.Get.Owner != owner) return false; if (!owner.Is0) { bool tm = owner.IsTopmost; if (tm != w.IsTopmost) { if (tm) w.ZorderTopmost(); else w.ZorderNoTopmost(); } if (!w.ZorderIsAbove(owner)) w.ZorderAbove(owner); } return true; } //probably not useful. Dangerous. ///// ///// Calls API SetClassLongPtr (SetClassLong in 32-bit process). ///// ///// //public static nint SetClassLong(wnd w, int index, nint newValue) //{ // lastError.clear(); // nint R = Api.SetClassLongPtr(w, index, newValue); // if(R == 0 && lastError.code != 0) w.ThrowUseNative(); // return R; //} //rejected. Does not work with many windows. Unreliable. Rarely used. ///// ///// Gets atom of a window class. ///// To get class atom when you have a window w, use WndUtil.GetClassLong(w, GCL.ATOM). ///// ///// Class name. ///// Native module handle of the exe or dll that registered the class. Don't use if it is a global class (CS_GLOBALCLASS style). //public static ushort GetClassAtom(string className, IntPtr moduleHandle = default) //{ // var x = new Api.WNDCLASSEX(); // x.cbSize = Api.SizeOf(x); // return Api.GetClassInfoEx(moduleHandle, className, ref x); //} /// /// Calls API RegisterWindowMessage. /// /// Message name. Can be any unique string. /// Also call API ChangeWindowMessageFilter for the message. More info: . public static int RegisterMessage(string name, bool uacEnable = false) { var m = Api.RegisterWindowMessage(name); if (uacEnable && m != 0) Api.ChangeWindowMessageFilter(m, 1); return m; } /// /// Calls API ChangeWindowMessageFilter for each message in the list of messages. /// It allows processes of lower [](xref:uac) integrity level to send these messages to this process. /// public static void UacEnableMessages(params int[] messages) { foreach (var m in messages) Api.ChangeWindowMessageFilter(m, 1); } #region print msg /// /// Writes a Windows message to a string. /// If the message is specified in options, sets s=null and returns false. /// public static bool PrintMsg(out string s, wnd w, int msg, nint wParam, nint lParam, PrintMsgOptions options = null, [CallerMemberName] string m_ = null) { //Could instead use System.Windows.Forms.Message.ToString, but its list is incomplete, eg no dpichange messages. // https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/MessageDecoder.cs,b19021e2f4480d57 if (options?.Skip is int[] a) { s = null; int prev = 0; foreach (var v in a) { if (v < 0) { if (msg >= prev && msg <= (v == int.MinValue ? int.MaxValue : -v)) return false; prev = int.MaxValue; } else { if (v == msg) return false; prev = v; } } } var (name, plus) = _Name(ref msg, out bool reflect); using (new StringBuilder_(out var b)) { if (options?.Number ?? true) { //uint counter = (uint)w.Prop["PrintMsg"]; w.Prop.Set("PrintMsg", ++counter); //b.Append(counter).Append(". "); b.Append(++s_pm_counter).Append(". "); } if (options?.Indent ?? true) { //makes ~10 times slower, but not too slow int i = 0; MethodBase m0 = null; foreach (var f in new StackTrace(1).GetFrames()) { var m1 = f.GetMethod(); if (m1.Name != m_) continue; if (m0 == null) m0 = m1; else if ((object)m1 == m0) i += 4; } if (i > 0) b.Append(' ', i); } if (reflect) b.Append("WM_REFLECT+"); if (name == null) b.AppendFormat("0x{0:X}", msg); else if (plus != 0) b.AppendFormat("{0}+0x{1:X}", name, plus); else if (msg >= 0xc000 && msg <= 0xffff) b.AppendFormat("\"{0}\"", name); else b.Append(name); b.AppendFormat(", 0x{0:X8}, 0x{1:X8}, hwnd={2}", (int)wParam, (int)lParam, w.Handle); if (options?.WindowProperties ?? false) { if (!w.Is0) b.AppendFormat(" ({0} \"{1}\" {{{2}}})", w.ClassName?.Limit(30), w.Name?.Limit(30), w.Rect.ToStringSimple()); } s = b.ToString(); return true; } static (string name, int plus) _Name(ref int m, out bool reflect) { reflect = false; if (m >= 0x10000) return default; //reserved by the system if (m >= 0xC000) return (ClipFormats.GetName(m, orNull: true), 0); //registered if (m >= Api.WM_APP) return ("WM_APP", m - Api.WM_APP); //0x8000 if (reflect = m >= Api.WM_REFLECT && m < Api.WM_REFLECT * 2) m -= Api.WM_REFLECT; //0x2000 if (m >= Api.WM_USER) return ("WM_USER", m - Api.WM_USER); //0x400 #region switch var s = m switch { 0x0 => "WM_NULL", 0x1 => "WM_CREATE", 0x2 => "WM_DESTROY", 0x3 => "WM_MOVE", 0x5 => "WM_SIZE", 0x6 => "WM_ACTIVATE", 0x7 => "WM_SETFOCUS", 0x8 => "WM_KILLFOCUS", 0xA => "WM_ENABLE", 0xB => "WM_SETREDRAW", 0xC => "WM_SETTEXT", 0xD => "WM_GETTEXT", 0xE => "WM_GETTEXTLENGTH", 0xF => "WM_PAINT", 0x10 => "WM_CLOSE", 0x11 => "WM_QUERYENDSESSION", 0x13 => "WM_QUERYOPEN", 0x16 => "WM_ENDSESSION", 0x12 => "WM_QUIT", 0x14 => "WM_ERASEBKGND", 0x15 => "WM_SYSCOLORCHANGE", 0x18 => "WM_SHOWWINDOW", 0x1A => "WM_SETTINGCHANGE", 0x1B => "WM_DEVMODECHANGE", 0x1C => "WM_ACTIVATEAPP", 0x1D => "WM_FONTCHANGE", 0x1E => "WM_TIMECHANGE", 0x1F => "WM_CANCELMODE", 0x20 => "WM_SETCURSOR", 0x21 => "WM_MOUSEACTIVATE", 0x22 => "WM_CHILDACTIVATE", 0x23 => "WM_QUEUESYNC", 0x24 => "WM_GETMINMAXINFO", 0x26 => "WM_PAINTICON", 0x27 => "WM_ICONERASEBKGND", 0x28 => "WM_NEXTDLGCTL", 0x2A => "WM_SPOOLERSTATUS", 0x2B => "WM_DRAWITEM", 0x2C => "WM_MEASUREITEM", 0x2D => "WM_DELETEITEM", 0x2E => "WM_VKEYTOITEM", 0x2F => "WM_CHARTOITEM", 0x30 => "WM_SETFONT", 0x31 => "WM_GETFONT", 0x32 => "WM_SETHOTKEY", 0x33 => "WM_GETHOTKEY", 0x37 => "WM_QUERYDRAGICON", 0x39 => "WM_COMPAREITEM", 0x3D => "WM_GETOBJECT", 0x41 => "WM_COMPACTING", 0x44 => "WM_COMMNOTIFY", 0x46 => "WM_WINDOWPOSCHANGING", 0x47 => "WM_WINDOWPOSCHANGED", 0x48 => "WM_POWER", 0x4A => "WM_COPYDATA", 0x4B => "WM_CANCELJOURNAL", 0x4E => "WM_NOTIFY", 0x50 => "WM_INPUTLANGCHANGEREQUEST", 0x51 => "WM_INPUTLANGCHANGE", 0x52 => "WM_TCARD", 0x53 => "WM_HELP", 0x54 => "WM_USERCHANGED", 0x55 => "WM_NOTIFYFORMAT", 0x7B => "WM_CONTEXTMENU", 0x7C => "WM_STYLECHANGING", 0x7D => "WM_STYLECHANGED", 0x7E => "WM_DISPLAYCHANGE", 0x7F => "WM_GETICON", 0x80 => "WM_SETICON", 0x81 => "WM_NCCREATE", 0x82 => "WM_NCDESTROY", 0x83 => "WM_NCCALCSIZE", 0x84 => "WM_NCHITTEST", 0x85 => "WM_NCPAINT", 0x86 => "WM_NCACTIVATE", 0x87 => "WM_GETDLGCODE", 0x88 => "WM_SYNCPAINT", 0xA0 => "WM_NCMOUSEMOVE", 0xA1 => "WM_NCLBUTTONDOWN", 0xA2 => "WM_NCLBUTTONUP", 0xA3 => "WM_NCLBUTTONDBLCLK", 0xA4 => "WM_NCRBUTTONDOWN", 0xA5 => "WM_NCRBUTTONUP", 0xA6 => "WM_NCRBUTTONDBLCLK", 0xA7 => "WM_NCMBUTTONDOWN", 0xA8 => "WM_NCMBUTTONUP", 0xA9 => "WM_NCMBUTTONDBLCLK", 0xAB => "WM_NCXBUTTONDOWN", 0xAC => "WM_NCXBUTTONUP", 0xAD => "WM_NCXBUTTONDBLCLK", 0xFE => "WM_INPUT_DEVICE_CHANGE", 0xFF => "WM_INPUT", 0x100 => "WM_KEYDOWN", 0x101 => "WM_KEYUP", 0x102 => "WM_CHAR", 0x103 => "WM_DEADCHAR", 0x104 => "WM_SYSKEYDOWN", 0x105 => "WM_SYSKEYUP", 0x106 => "WM_SYSCHAR", 0x107 => "WM_SYSDEADCHAR", 0x109 => "WM_UNICHAR", 0x10D => "WM_IME_STARTCOMPOSITION", 0x10E => "WM_IME_ENDCOMPOSITION", 0x10F => "WM_IME_COMPOSITION", 0x110 => "WM_INITDIALOG", 0x111 => "WM_COMMAND", 0x112 => "WM_SYSCOMMAND", 0x113 => "WM_TIMER", 0x114 => "WM_HSCROLL", 0x115 => "WM_VSCROLL", 0x116 => "WM_INITMENU", 0x117 => "WM_INITMENUPOPUP", 0x119 => "WM_GESTURE", 0x11A => "WM_GESTURENOTIFY", 0x11F => "WM_MENUSELECT", 0x120 => "WM_MENUCHAR", 0x121 => "WM_ENTERIDLE", 0x122 => "WM_MENURBUTTONUP", 0x123 => "WM_MENUDRAG", 0x124 => "WM_MENUGETOBJECT", 0x125 => "WM_UNINITMENUPOPUP", 0x126 => "WM_MENUCOMMAND", 0x127 => "WM_CHANGEUISTATE", 0x128 => "WM_UPDATEUISTATE", 0x129 => "WM_QUERYUISTATE", 0x132 => "WM_CTLCOLORMSGBOX", 0x133 => "WM_CTLCOLOREDIT", 0x134 => "WM_CTLCOLORLISTBOX", 0x135 => "WM_CTLCOLORBTN", 0x136 => "WM_CTLCOLORDLG", 0x137 => "WM_CTLCOLORSCROLLBAR", 0x138 => "WM_CTLCOLORSTATIC", 0x200 => "WM_MOUSEMOVE", 0x201 => "WM_LBUTTONDOWN", 0x202 => "WM_LBUTTONUP", 0x203 => "WM_LBUTTONDBLCLK", 0x204 => "WM_RBUTTONDOWN", 0x205 => "WM_RBUTTONUP", 0x206 => "WM_RBUTTONDBLCLK", 0x207 => "WM_MBUTTONDOWN", 0x208 => "WM_MBUTTONUP", 0x209 => "WM_MBUTTONDBLCLK", 0x20A => "WM_MOUSEWHEEL", 0x20B => "WM_XBUTTONDOWN", 0x20C => "WM_XBUTTONUP", 0x20D => "WM_XBUTTONDBLCLK", 0x20E => "WM_MOUSEHWHEEL", 0x210 => "WM_PARENTNOTIFY", 0x211 => "WM_ENTERMENULOOP", 0x212 => "WM_EXITMENULOOP", 0x213 => "WM_NEXTMENU", 0x214 => "WM_SIZING", 0x215 => "WM_CAPTURECHANGED", 0x216 => "WM_MOVING", 0x218 => "WM_POWERBROADCAST", 0x219 => "WM_DEVICECHANGE", 0x220 => "WM_MDICREATE", 0x221 => "WM_MDIDESTROY", 0x222 => "WM_MDIACTIVATE", 0x223 => "WM_MDIRESTORE", 0x224 => "WM_MDINEXT", 0x225 => "WM_MDIMAXIMIZE", 0x226 => "WM_MDITILE", 0x227 => "WM_MDICASCADE", 0x228 => "WM_MDIICONARRANGE", 0x229 => "WM_MDIGETACTIVE", 0x230 => "WM_MDISETMENU", 0x231 => "WM_ENTERSIZEMOVE", 0x232 => "WM_EXITSIZEMOVE", 0x233 => "WM_DROPFILES", 0x234 => "WM_MDIREFRESHMENU", 0x238 => "WM_POINTERDEVICECHANGE", 0x239 => "WM_POINTERDEVICEINRANGE", 0x23A => "WM_POINTERDEVICEOUTOFRANGE", 0x240 => "WM_TOUCH", 0x241 => "WM_NCPOINTERUPDATE", 0x242 => "WM_NCPOINTERDOWN", 0x243 => "WM_NCPOINTERUP", 0x245 => "WM_POINTERUPDATE", 0x246 => "WM_POINTERDOWN", 0x247 => "WM_POINTERUP", 0x249 => "WM_POINTERENTER", 0x24A => "WM_POINTERLEAVE", 0x24B => "WM_POINTERACTIVATE", 0x24C => "WM_POINTERCAPTURECHANGED", 0x24D => "WM_TOUCHHITTESTING", 0x24E => "WM_POINTERWHEEL", 0x24F => "WM_POINTERHWHEEL", 0x251 => "WM_POINTERROUTEDTO", 0x252 => "WM_POINTERROUTEDAWAY", 0x253 => "WM_POINTERROUTEDRELEASED", 0x281 => "WM_IME_SETCONTEXT", 0x282 => "WM_IME_NOTIFY", 0x283 => "WM_IME_CONTROL", 0x284 => "WM_IME_COMPOSITIONFULL", 0x285 => "WM_IME_SELECT", 0x286 => "WM_IME_CHAR", 0x288 => "WM_IME_REQUEST", 0x290 => "WM_IME_KEYDOWN", 0x291 => "WM_IME_KEYUP", 0x2A1 => "WM_MOUSEHOVER", 0x2A3 => "WM_MOUSELEAVE", 0x2A0 => "WM_NCMOUSEHOVER", 0x2A2 => "WM_NCMOUSELEAVE", 0x2B1 => "WM_WTSSESSION_CHANGE", 0x2E0 => "WM_DPICHANGED", 0x2E2 => "WM_DPICHANGED_BEFOREPARENT", 0x2E3 => "WM_DPICHANGED_AFTERPARENT", 0x2E4 => "WM_GETDPISCALEDSIZE", 0x300 => "WM_CUT", 0x301 => "WM_COPY", 0x302 => "WM_PASTE", 0x303 => "WM_CLEAR", 0x304 => "WM_UNDO", 0x305 => "WM_RENDERFORMAT", 0x306 => "WM_RENDERALLFORMATS", 0x307 => "WM_DESTROYCLIPBOARD", 0x308 => "WM_DRAWCLIPBOARD", 0x309 => "WM_PAINTCLIPBOARD", 0x30A => "WM_VSCROLLCLIPBOARD", 0x30B => "WM_SIZECLIPBOARD", 0x30C => "WM_ASKCBFORMATNAME", 0x30D => "WM_CHANGECBCHAIN", 0x30E => "WM_HSCROLLCLIPBOARD", 0x30F => "WM_QUERYNEWPALETTE", 0x310 => "WM_PALETTEISCHANGING", 0x311 => "WM_PALETTECHANGED", 0x312 => "WM_HOTKEY", 0x317 => "WM_PRINT", 0x318 => "WM_PRINTCLIENT", 0x319 => "WM_APPCOMMAND", 0x31A => "WM_THEMECHANGED", 0x31D => "WM_CLIPBOARDUPDATE", 0x31E => "WM_DWMCOMPOSITIONCHANGED", 0x31F => "WM_DWMNCRENDERINGCHANGED", 0x320 => "WM_DWMCOLORIZATIONCOLORCHANGED", 0x321 => "WM_DWMWINDOWMAXIMIZEDCHANGE", 0x323 => "WM_DWMSENDICONICTHUMBNAIL", 0x326 => "WM_DWMSENDICONICLIVEPREVIEWBITMAP", 0x33F => "WM_GETTITLEBARINFOEX", 0x8000 => "WM_APP", 0x400 => "WM_USER", _ => null }; #endregion return (s, 0); } } [ThreadStatic] static uint s_pm_counter; #if !true //this script creates the switch { ... } //var a=new List(); print.clear(); var b = new StringBuilder("var s = m switch {\r\n"); var s1 = File.ReadAllText(@"C:\code\au\Au\Api\Api_const.cs"); foreach (var m in s1.RxFindAll(@"(?m)^\h*internal const int (WM_\w+) *= *(\w+);")) { var s = m[1].Value; if (s.Ends("FIRST") || s.Ends("LAST") || s.Starts("WM_PSD_") || s.Starts("WM_DDE_") || s.Starts("WM_CHOOSEFONT_") || s == "WM_WININICHANGE") { //print.it(s); continue; } //print.it(s, m[2]); //a.Add(s); b.AppendFormat("{0} => \"{1}\",\r\n", m[2].Value, s); } b.Append("_ => null};\r\nreturn (s, 0);"); //a.Sort(); //print.it(a); var s2 = b.ToString(); print.it(s2); #endif /// /// Writes a Windows message to the output, unless it is specified in options. /// public static void PrintMsg(wnd w, int msg, nint wParam, nint lParam, PrintMsgOptions options = null, [CallerMemberName] string m_ = null) { if (PrintMsg(out string s, w, msg, wParam, lParam, options, m_)) print.it(s); } /// /// Writes a Windows message to a string. /// If the message is specified in options, sets s=null and returns false. /// /// /// The m parameter also accepts (WPF) and . /// public static bool PrintMsg(out string s, in MSG m, PrintMsgOptions options = null, [CallerMemberName] string m_ = null) { return PrintMsg(out s, m.hwnd, m.message, m.wParam, m.lParam, options, m_); } /// /// Writes a Windows message to the output, unless it is specified in options. /// /// /// The m parameter also accepts (WPF) and . /// public static void PrintMsg(in MSG m, PrintMsgOptions options = null, [CallerMemberName] string m_ = null) { PrintMsg(m.hwnd, m.message, m.wParam, m.lParam, options, m_); } #endregion /// /// Simple non-OLE drag operation. /// /// true if dropped, false if canceled. /// Window or control that owns the drag operation. Must be of this thread. /// Mouse button that is used for the drag operation: Left, Right, Middle. /// Callback function, called on each received mouse/key message. Optional. public static bool DragLoop(AnyWnd window, MButtons mouseButton = MButtons.Left, Action onMouseKeyMessage = null) { wnd w = window.Hwnd; Api.SetCapture(w); bool R = false; var x = new WDLArgs(); for (; ; ) { if (Api.GetCapture() != w) return false; if (!Api.GetMessage(out x.msg)) { if (x.msg.message == Api.WM_QUIT) Api.PostQuitMessage((int)x.msg.wParam); break; } bool call = false; int m = x.msg.message; if (m >= Api.WM_MOUSEFIRST && m <= Api.WM_MOUSELAST) { if (m == Api.WM_LBUTTONUP) { if (R = mouseButton.Has(MButtons.Left)) break; } else if (m == Api.WM_RBUTTONUP) { if (R = mouseButton.Has(MButtons.Right)) break; } else if (m == Api.WM_MBUTTONUP) { if (R = mouseButton.Has(MButtons.Middle)) break; } call = true; } else if (m == Api.WM_KEYDOWN || m == Api.WM_KEYUP || m == Api.WM_SYSKEYDOWN || m == Api.WM_SYSKEYUP) { //on key down/up caller may want to update cursor when eg Ctrl pressed/released if (x.msg.wParam == (byte)KKey.Escape) break; call = true; } if (call && onMouseKeyMessage != null) { onMouseKeyMessage(x); if (x._stopped) break; if (x.cursor != default) { Api.SetCursor(x.cursor); x.cursor = default; } } Api.DispatchMessage(x.msg); } Api.ReleaseCapture(); return R; } /// /// Waits while there is no active window. /// /// While waiting call to process Windows messages etc. /// /// When there is no active window, functions and API GetForegroundWindow return 0. /// It sometimes happens after closing, minimizing or switching the active window, briefly until another window becomes active. /// This function waits max 500 ms, then returns false if there is no active window. /// Don't need to call this after calling functions of this library. /// public static bool WaitForAnActiveWindow(bool doEvents = false) { for (int i = 0; ;) { if (doEvents) wait.doEvents(); if (!wnd.active.Is0) return true; if (++i == 32) break; wait.ms(i); } return false; //Call WaitForAnActiveWindow(true) after showing a dialog API. // In a thread that does not process messages, after closing a dialog may be not updated key states. // Processing remaining unprocessed messages fixes it. } /// /// Temporarily enables this process to activate windows with API SetForegroundWindow. /// /// false if failed. /// Process id. If not 0, enables that process to activate windows too. If -1, all processes will be enabled. /// /// In some cases you may need this function because Windows often disables API SetForegroundWindow to not allow background applications to activate windows while the user is working (using keyboard/mouse) with the currently active window. Then SetForegroundWindow usually just makes the window's taskbar button flash. /// Usually you don't call SetForegroundWindow directly. It is called by some other functions. /// Don't need to call this function before calling and other functions of this library that activate windows. /// public static bool EnableActivate(int processId = 0) { if (!wnd.Internal_.EnableActivate(false)) return false; return processId == 0 || Api.AllowSetForegroundWindow(processId); } /// /// Calls API PostThreadMessage. /// /// false if failed. Supports . public static bool PostThreadMessage(int threadId, int message, nint wParam = 0, nint lParam = 0) { return Api.PostThreadMessage(threadId, message, wParam, lParam); } /// /// Subclasses a window. /// /// A window or control of this thread. /// The new window procedure. It is called on every message received by the window (unless blocked by another subclass added later). Let it call , except when you want to block the message. /// A cookie for . Returns null if failed. /// /// Uses API SetWindowSubclass. /// Implicitly unsubclasses when the window is destroyed. /// Protects proc from GC for as long as need. /// public static object Subclass(wnd w, WNDPROC proc) { Api.SUBCLASSPROC sp = null; if (!Api.SetWindowSubclass(w, sp = _WndProc, 1)) return null; (t_asp ??= new()).Add(sp); nint _WndProc(wnd w, int msg, nint wp, nint lp, nint idSubclass, nint refData) { var r = proc(w, msg, wp, lp); if (msg == Api.WM_NCDESTROY) if (Api.RemoveWindowSubclass(w, sp, 1)) t_asp.Remove(sp); return r; } return new _SubclassCookie(w, sp); } [ThreadStatic] static List t_asp; //GC record class _SubclassCookie(wnd w, Api.SUBCLASSPROC sp); /// /// Unsubclasses window subclassed by . /// Unsubclassing is optional; the window is implicitly unsubclassed when closed. /// /// The return value of . public static void Unsubclass(object cookie) { if (cookie == null) return; if (cookie is not _SubclassCookie c) throw new ArgumentException(); if (Api.RemoveWindowSubclass(c.w, c.sp, 1) || !c.w.IsAlive) t_asp.Remove(c.sp); } /// /// Let your callback function used with call this function and return its return value. After or before processing the message. /// public static nint DefSubclassProc(wnd w, int msg, nint wp, nint lp) => Api.DefSubclassProc(w, msg, wp, lp); } } namespace Au.Types { /// /// Options for . /// public class PrintMsgOptions { /// public PrintMsgOptions() { } /// /// Sets . /// public PrintMsgOptions(params int[] skip) { Skip = skip; } /// /// Prepend counter 1, 2, 3... /// Default true. /// public bool Number { get; set; } = true; /// /// Prepend one or more tabs if the caller function (usually a window procedure) is called recursively. /// Default true. /// public bool Indent { get; set; } = true; /// /// Ignore these messages. /// To specify a range of messages, use two array elements: first message and negative last message. /// public int[] Skip { get; set; } /// /// Append window classname, name and rectangle. /// public bool WindowProperties { get; set; } } /// /// callback function arguments. /// public class WDLArgs { /// /// Current message retrieved by API GetMessage. /// API MSG. /// public MSG msg; /// /// Native cursor handle. The callback function can set this to temporarily set cursor. /// public IntPtr cursor; /// /// The callback function can call this to end the operation. /// public void Stop() { _stopped = true; } internal bool _stopped; } /// /// Used with . /// public class RWCEtc { #pragma warning disable 1591 //XML doc public uint style; public int cbClsExtra; public int cbWndExtra; public IntPtr hIcon; public IntPtr hCursor; public MCursor mCursor; public nint hbrBackground; public IntPtr hIconSm; #pragma warning restore 1591 //XML doc } } ================================================ FILE: Au/wnd/inactive/desktop.cs ================================================ //rejected. // Existing functions are rarely used and can be invoked with hotkeys, eg Win+D = Show Desktop, Win+Tab = Task View, Win+M = Minimize All, Win+Shift+M = Undo. // Maybe will be added in the future, when added more functions, eg for virtual desktops. #if !true namespace Au { /// /// Arranges windows, shows/hides desktop. The same as the taskbar right-click menu commands. /// public static class desktop { /// /// Shows or hides desktop. /// If there are non-minimized main windows, minimizes them. Else restores windows recently minimized by this function. /// public static void toggleShowDesktop() => _Do(0); /// /// Minimizes main windows. /// public static void minimizeWindows() => _Do(1); /// /// Cascades non-minimized main windows. /// public static void cascadeWindows() => _Do(3); /// /// Arranges non-minimized main windows horizontally or vertically. /// public static void tileWindows(bool vertically) => _Do(vertically ? 5 : 4); /// /// Restores windows recently minimized, cascaded or tiled with other functions of this class. /// public static void undoMinimizeEtc() => _Do(2); /// /// Shows the Task View, aka Window Switcher. /// May not work on some Windows versions. /// public static void taskView() => _Do(6); static void _Do(int what) { try { dynamic shell = Activator.CreateInstance(Type.GetTypeFromProgID("Shell.Application")); //speed: faster than calling a method switch (what) { case 0: shell.ToggleDesktop(); break; case 1: shell.MinimizeAll(); break; case 2: shell.UndoMinimizeALL(); break; case 3: shell.CascadeWindows(); break; case 4: shell.TileHorizontally(); break; case 5: shell.TileVertically(); break; case 6: shell.WindowSwitcher(); break; } Marshal.ReleaseComObject(shell); //The COM object does not do exactly the same as the true Explorer commands. //Eg MinimizeAll does not activete desktop. Then a minimized window is active. if (what == 1 && wnd.active.IsMinimized && wnd.getwnd.desktop(out var wd, out _)) wd.ActivateL(); } catch { } wnd.getwnd.shellWindow.MinimalSleepIfOtherThread_(); } } //FUTURE: use IVirtualDesktopManager to manage virtual desktops. //Its MoveWindowToDesktop does not work with windows of other processes, but we can inject a dll into another process. //The inteface also has IsWindowOnCurrentVirtualDesktop and GetWindowDesktopId. //Also there are internal/undocumented interfaces to add/remove/switch desktops etc. There is a GitHub library. And another library that injects. } #endif ================================================ FILE: Au/wnd/wnd.cs ================================================ namespace Au; /// /// A variable of wnd type represents a window or control. It is a window handle, also known as HWND. /// /// /// wnd functions can be used with windows and controls of any process/thread. Also can be used with .NET form/control and WPF window class variables, like wnd w=form.Hwnd(); w.Method(...);. /// /// There are two main types of windows - top-level windows and controls. Controls are child windows of top-level windows. /// /// More window functions are in types named like WndX, in namespace Au.More. They are used mostly in programming, rarely in automation scripts. /// /// What happens when a wnd member function fails: /// - Functions that get window properties don't throw exceptions. They return false/0/null/empty. Most of them support , and it is mentioned in function documentation. /// - Many functions that change window properties throw exception. Exceptions are listed in function documentation. Almost all these functions throw only . /// - Other functions that change window properties return false. They are more often used in programming than in automation scripts. /// - When a "find" function does not find the window or control, it returns default(wnd) (window handle 0). Then will return true. /// - If a function does not follow these rules, it is mentioned in function documentation. /// /// Many functions fail if the window's process has a higher [](xref:uac) integrity level (administrator, uiAccess) than this process, unless this process has uiAccess level. Especially the functions that change window properties. Some functions that still work: Activate, ActivateL, ShowMinimized, ShowNotMinimized, ShowNotMinMax, Close. /// /// The wnd type can be used with native Windows API functions without casting. Use wnd for the parameter type in the declaration, like [DllImport(...)] static extern bool NativeFunction(wnd hWnd, ...). /// /// See also: Window Features. /// /// /// /// public unsafe partial struct wnd : IEquatable, IComparable { #if false /// /// Why wnd is struct, not class: /// Advantages: /// - Lightweight. Same as nint (8 or 4 bytes). /// - Easier to create overloaded functions that have a window parameter. If it was a class, then a null argument could be ambiguous if eg could also be a string etc. /// - When a find-window function does not find the window, calling next function (without checking the return value) does not throw null-reference exception. Instead the function can throw a more specific exception or just return false etc. /// - The handle actually already is a reference (to a window object managed by the OS). We don't own the object; we usually don't need to destroy the window finally; it is more like a numeric window id. /// - Code where a window argument is default(wnd) is more clear. If it would be null, then it is unclear how the function interprets it: as a 0 handle or as "don't use it". Now if we want a "don't use it" behavior, we'll use an overload. /// - In my experience, it makes programming/scripting easier than if it would be a class. Because windows are not found so often (in automation scripts). A find-window function could throw a "not found" exception, but it is not good (it's easier to check the return value than to use try/catch or throwing/non-throwing overloads). /// - Probably it is not a "bad practice" to have a struct with many member functions, because eg the .NET DateTime is struct. /// /// Disadvantages: /// - Cannot be a base class of other classes. Workaround: Use it as a public field or property of the other class (or struct); in some cases it can be even better, because wnd has very many methods, and the non-inherited methods of that class would be difficult to find; now they are separated, and can be used like x.NewClassMethod() and x.w.WndMethod(); anyway, in most cases we'll need the new window classes only for the functions that they add, not for wnd functions, eg we would use a class ButtonWnd mostly only for button functions, not for general window functions. /// - In some cases C# does not allow to call a property-set function. wnd has few such functions, maybe none. /// /// //note: don't use :IWin32Window, because it loads System.Windows.Forms.dll always when wnd used. #endif readonly nint _h; #region constructors, operators, overrides #pragma warning disable 1591 //XML doc public wnd(nint hwnd) { _h = hwnd; } //note: don't need implicit conversions. It creates more problems than is useful. public static explicit operator wnd(nint hwnd) => new(hwnd); public static explicit operator nint(wnd w) => w.Handle; public static explicit operator wnd(int hwnd) => new(hwnd); public static explicit operator int(wnd w) => (int)w._h; /// /// Converts from a special handle value. /// /// See API SetWindowPos. public static implicit operator wnd(SpecHWND hwnd) => new((int)hwnd); /// Compares window handles. public static bool operator ==(wnd w1, wnd w2) => w1._h == w2._h; /// Compares window handles. public static bool operator !=(wnd w1, wnd w2) => w1._h != w2._h; //Prevent accidental usage wnd==null. The C# compiler used to allow it without a warning; level-5 warning since C# 9. Bad: disables wnd==wnd?. //[Obsolete("Replace wnd==wnd? with wnd.Equals(wnd?). Replace wnd==null with wnd.Is0.", true), NoDoc] //public static bool operator ==(wnd w1, wnd? w2) => false; //[Obsolete("Replace wnd==wnd? with wnd.Equals(wnd?). Replace wnd==null with wnd.Is0.", true), NoDoc] //public static bool operator !=(wnd w1, wnd? w2) => true; //[Obsolete("Replace wnd==wnd? with wnd.Equals(wnd?). Replace wnd==null with wnd.Is0.", true), NoDoc] //public static bool operator ==(wnd? w1, wnd w2) => false; //[Obsolete("Replace wnd==wnd? with wnd.Equals(wnd?). Replace wnd==null with wnd.Is0.", true), NoDoc] //public static bool operator !=(wnd? w1, wnd w2) => true; #pragma warning restore 1591 //XML doc /// /// Returns true if w != null and w.Value == this. /// public bool Equals(wnd? w) => w != null && w.GetValueOrDefault() == this; /// /// Returns true if obj is and contains the same window handle. /// public override bool Equals(object obj) => obj is wnd w && this == w; /// /// Returns true if other == this. /// public bool Equals(wnd other) => other == this; //IEquatable.Equals, to avoid boxing with eg Dictionary /// /// Implements . It allows to sort a collection. /// public int CompareTo(wnd other) => _h.CompareTo(other); /// public override int GetHashCode() => (int)_h; //window handles are always 32-bit int, although in a 64-bit process stored in 64-bit variables. /// /// Gets window handle as IntPtr. /// Code w.Handle is the same as (IntPtr)w . /// public IntPtr Handle => _h; /// /// Formats string $"{handle} {ClassName} \"{Name}\" {ProgramName} {Rect}". /// public override string ToString() { if (Is0) return "0"; var cn = ClassName; var sh = Handle.ToString(); if (cn == null) return sh + " "; string s = Name; if (s != null) s = s.Escape(limit: 250); return $"{sh} {cn} \"{s}\" {ProgramName} {Rect.ToString()}"; } #endregion #region send/post message /// /// Calls API SendMessage. /// /// Supports . public nint Send(int message, nint wParam = 0, nint lParam = 0) { Debug_.PrintIf(Is0); return Api.SendMessage(this, message, wParam, lParam); } /// /// Calls API SendMessage where lParam is string. /// /// Supports . public nint Send(int message, nint wParam, string lParam) { Debug_.PrintIf(Is0); fixed (char* p = lParam) return Api.SendMessage(this, message, wParam, (nint)p); //info: don't use overload, then eg ambiguous if null. } /// /// Calls API SendMessage where lParam is any pointer. /// /// Supports . public nint Send(int message, nint wParam, void* lParam) { Debug_.PrintIf(Is0); return Api.SendMessage(this, message, wParam, (nint)lParam); //info: don't use overload, then eg ambiguous if null. } //rejected: overload without out result parameter. Confusing intellisense when bad types of other parameters. Not so often used, and not so hard to put ', out _'. ///// ///// Calls API SendMessageTimeout. ///// Returns its return value (false if failed). Supports . ///// //public bool SendTimeout(int millisecondsTimeout, int message, nint wParam = 0, nint lParam = 0, SMTFlags flags = SMTFlags.ABORTIFHUNG) { // Debug_.PrintIf(Is0); // return 0 != Api.SendMessageTimeout(this, message, wParam, lParam, flags, millisecondsTimeout, out _); //} /// /// Calls/returns API SendMessageTimeout and gets the result of the message processing. /// /// false if failed. Supports . public bool SendTimeout(int millisecondsTimeout, out nint result, int message, nint wParam = 0, nint lParam = 0, SMTFlags flags = SMTFlags.ABORTIFHUNG) { Debug_.PrintIf(Is0); return 0 != Api.SendMessageTimeout(this, message, wParam, lParam, flags, millisecondsTimeout, out result); } /// /// Calls/returns API SendMessageTimeout where lParam is string. /// /// false if failed. Supports . public bool SendTimeout(int millisecondsTimeout, out nint result, int message, nint wParam, string lParam, SMTFlags flags = SMTFlags.ABORTIFHUNG) { Debug_.PrintIf(Is0); result = 0; fixed (char* p = lParam) return 0 != Api.SendMessageTimeout(this, message, wParam, (nint)p, flags, millisecondsTimeout, out result); } /// /// Calls/returns API SendMessageTimeout and gets the result of the message processing. /// /// false if failed. Supports . public bool SendTimeout(int millisecondsTimeout, out nint result, int message, nint wParam, void* lParam, SMTFlags flags = SMTFlags.ABORTIFHUNG) { Debug_.PrintIf(Is0); return 0 != Api.SendMessageTimeout(this, message, wParam, (nint)lParam, flags, millisecondsTimeout, out result); } /// /// Calls/returns API SendNotifyMessage. /// /// false if failed. Supports . public bool SendNotify(int message, nint wParam = 0, nint lParam = 0) { Debug_.PrintIf(Is0); return Api.SendNotifyMessage(this, message, wParam, lParam); } /// /// Calls/returns API PostMessage. /// /// false if failed. Supports . /// public bool Post(int message, nint wParam = 0, nint lParam = 0) { //Debug.Assert(!Is0); //no, can be used for "post thread message" return Api.PostMessage(this, message, wParam, lParam); } #endregion #region throw, valid /// /// If , throws . /// /// This. /// public wnd ThrowIf0() { if (Is0) throw new AuWndException(this, Api.ERROR_INVALID_WINDOW_HANDLE); return this; } /// /// If or not , throws . /// /// This. /// public wnd ThrowIfInvalid() { if (Is0 || !Api.IsWindow(this)) throw new AuWndException(this, Api.ERROR_INVALID_WINDOW_HANDLE); return this; } ///// ///// If , throws . Returns !IsAlive. ///// ///// //public bool IsInvalidThrowIf0() //{ // if(Is0) throw new AuWndException(this, Api.ERROR_INVALID_WINDOW_HANDLE); // return !Api.IsWindow(this); //} //CONSIDER: in many places replace ThrowIfInvalid with ThrowIf0 or IsInvalidThrowIf0. /// /// Throws that uses the last Windows API error (code and message). /// Also the message depends on whether the window handle is 0/invalid. /// /// public void ThrowUseNative() { throw new AuWndException(this, 0); } /// /// Throws that uses the specified Windows API error code and its message. /// Also the message depends on whether the window handle is 0/invalid. /// /// public void ThrowUseNative(int errorCode) { throw new AuWndException(this, errorCode); } /// /// Throws that uses mainMessage and the last Windows API error (code and message). /// Also the message depends on whether the window handle is 0/invalid. /// /// public void ThrowUseNative(string mainMessage) { throw new AuWndException(this, 0, mainMessage); } /// /// Throws that uses mainMessage and the specified Windows API error code. /// Also the message depends on whether the window handle is 0/invalid. /// /// public void ThrowUseNative(int errorCode, string mainMessage) { throw new AuWndException(this, errorCode, mainMessage); } /// /// Throws that uses mainMessage and does not use the last Windows API error. /// Also the message depends on whether the window handle is 0/invalid. /// /// public void ThrowNoNative(string mainMessage) { throw new AuWndException(this, mainMessage); } /// /// Returns true if the window handle is 0 (this variable == default(wnd)). /// /// /// /// /// public bool Is0 => _h == default; /// /// Returns true if the window exists (the window handle is valid). /// Returns false if the handle is 0 or invalid. /// Invalid non-0 handle usually means that the window is closed/destroyed. /// /// /// Calls and API IsWindow. /// Although a variable holds a window handle, which is like a reference to a window, it does not prevent closing that window and making the handle invalid. After closing the window, the OS can even assign the same handle value to a new window, although normally it can happen only after long time. /// Use this carefully with windows of other applications or threads. The window can be closed at any moment, even when your thread is still in this function. /// public bool IsAlive => !Is0 && Api.IsWindow(this); #endregion #region visible, enabled, cloaked /// /// Returns true if the window is visible. /// Returns false if is invisible or is a child of invisible parent. /// Also returns false when fails (probably window closed or 0 handle). Supports . /// /// /// Calls API IsWindowVisible. Does not call . /// /// Even when this function returns true, the window may be actually invisible. It can be cloaked, on an inactive Windows 10 virtual desktop (cloaked), inactive Windows Store app (cloaked), transparent, zero-size, minimized, off-screen, covered by other windows or can have zero-size window region. /// /// /// /// /// public bool IsVisible => Api.IsWindowVisible(this); //rejected. Unreliable, eg then does not find Store apps in inactive desktops. Instead now Find skips all cloaked by default. ///// ///// Returns true if the window is visible. ///// Returns false if is invisible or is a child of invisible parent. ///// Also returns false when fails (probably window closed or 0 handle). Supports . ///// ///// ///// Returns false if API IsWindowVisible returns false. ///// Also returns false if returns true, but only for some popup windows that usually are useless and could cause problems if considered visible. ///// Else returns true. ///// ///// Even when this function returns true, the window may be actually invisible. It can be cloaked (except the above case), on an inactive Windows 10 virtual desktop (cloaked), inactive Windows Store app (cloaked), transparent, zero-size, minimized, off-screen, covered by other windows or can have zero-size window region. ///// ///// ///// ///// ///// ///// //public bool IsVisibleEx { // get { // if(!Api.IsWindowVisible(this)) return false; // var style = Style; // if((style & (WS.POPUP | WS.CHILD)) == WS.POPUP) { // if((style & WS.CAPTION) != WS.CAPTION) return !IsCloaked; // //is it a ghost ApplicationFrameWindow, like closed Calculator on Win10? // if(osVersion.minWin10 && HasExStyle(WSE.NOREDIRECTIONBITMAP) && IsCloaked && ClassNameIs("ApplicationFrameWindow")) { // var isGhost = default == Api.FindWindowEx(this, default, "Windows.UI.Core.CoreWindow", null); // //print.it(isGhost, this); // return !isGhost; // } // } // return true; // } //} /// /// Returns true if returns true and returns false. /// public bool IsVisibleAndNotCloaked => IsVisible && !IsCloaked; /// /// Returns true if this window is visible in the specified parent or ancestor window. /// Like , but does not check the visibility of the specified parent/ancestor window. /// /// Parent or ancestor window. internal bool IsVisibleIn_(wnd wParent) { if (IsVisible) return true; //these two make faster in most cases (when wTL is visible) if (wParent.IsVisible) return false; var c = this; for (int i = 0; i < 10000; i++) { //print.it(c); if (!c.HasStyle(WS.VISIBLE)) return false; c = c.ParentGWL_; if (c == wParent) return true; if (c.Is0) break; } Debug.Assert(false); return false; } /// /// Shows (if hidden) or hides this window. /// /// /// Does not activate/deactivate/zorder. /// This window can be of any thread. If you know it is of this thread, use . This function calls it. /// /// public void Show(bool show) { if (!ShowL(show)) ThrowUseNative(show ? "*show*" : "*hide*"); MinimalSleepIfOtherThread_(); } /// /// Shows (if hidden) or hides this window. /// /// Returns false if failed. Supports . /// /// Does not activate/deactivate/zorder. /// /// There are two similar functions to show/hide a window: /// - is better to use in automation scripts, with windows of any process/thread. It calls ShowL, and throws exception if it fails. Adds a small delay if the window is of another thread. /// - ShowL is better to use in programming, with windows of current thread. It is more lightweight. Does not throw exceptions. Does not add a delay. But both functions can be used with windows of any thread. /// public bool ShowL(bool show) { if (show == HasStyle(WS.VISIBLE)) return true; //avoid messages and make much faster. Never mind: if show==false, returns true even if invalid hwnd. Send(Api.WM_SHOWWINDOW, show ? 1 : 0); //not necessary for most windows, but eg winforms would not update the Visible property without it. ShowWindow sends it but SetWindowPos doesn't. return SetWindowPos((show ? SWPFlags.SHOWWINDOW : SWPFlags.HIDEWINDOW) | SWPFlags.NOSIZE | SWPFlags.NOMOVE | SWPFlags.NOACTIVATE | SWPFlags.NOZORDER | SWPFlags.NOOWNERZORDER); //This code is similar to ShowWindow(SW_SHOWNA). Don't use it because: // May change Z order. Eg makes above other windows of same thread. // Also may not work first time in process. The documentation is unclear. Not tested. } /// /// Returns true if the window is enabled for mouse and keyboard input. /// Returns false if disabled. Also false if failed (probably window closed or 0 handle). Supports . /// /// Check whether all ancestors of this control are enabled too. If false (default), this function simply calls API IsWindowEnabled, which usually returns true for controls in disabled windows. public bool IsEnabled(bool ancestorsToo = false) { if (!ancestorsToo) return Api.IsWindowEnabled(this); for (var w = this; ;) { if (!Api.IsWindowEnabled(w)) return false; w = w.Get.DirectParent; if (w.Is0) break; } return true; } /// /// Enables or disables. /// Calls API EnableWindow. /// /// Enable or disable. /// public void Enable(bool enable) { lastError.clear(); Api.EnableWindow(this, enable); if (lastError.code != 0) ThrowUseNative("*enable/disable*"); } /// /// Gets the cloaked state (it's a way to hide a window). /// /// ///
• 0 if not cloaked or if failed. ///
• 1 cloaked by its application. ///
• 2 cloaked by Windows. ///
• 4 cloaked because its owner window is cloaked. /// /// /// On Windows 7 returns 0 because there is no "cloaked window" feature. /// ///
/// public int IsCloakedGetState { get { if (!osVersion.minWin8) return 0; int cloaked = 0; int hr = Api.DwmGetWindowAttribute(this, Api.DWMWA.CLOAKED, &cloaked, 4); return cloaked; } } /// /// Detects whether the window is cloaked (it's a way to hide a window). /// /// true if the window is cloaked. false if not cloaked or if failed. /// /// On Windows 7 returns false because there is no "cloaked window" feature. /// Windows 10 uses window cloaking mostly to hide windows on inactive desktops. Windows 8 - mostly to hide Windows Store app windows. /// /// public bool IsCloaked => IsCloakedGetState != 0; #endregion #region minimized, maximized /// /// Returns true if minimized, false if not. /// Also returns false when fails (probably window closed or 0 handle). Supports . /// Calls API IsIconic. /// public bool IsMinimized => Api.IsIconic(this); /// /// Returns true if maximized, false if not. /// Also returns false when fails (probably window closed or 0 handle). Supports . /// Calls API IsZoomed. /// public bool IsMaximized => Api.IsZoomed(this); /// /// If not minimized, minimizes. /// Also unhides. /// /// What API to use: 0 ShowWindow, 1 SetWindowPlacement (no animation), 2 WM_SYSCOMMAND. /// The API call failed. No exception if the window did not obey. public void ShowMinimized(int how = 0) => _MinMaxRes(Api.SW_MINIMIZE, how); /// /// If not minimized, minimizes. /// Also unhides. /// /// What API to use: 0 ShowWindow, 1 SetWindowPlacement (no animation), 2 WM_SYSCOMMAND. /// The API call failed. No exception if the window did not obey. public void ShowMaximized(int how = 0) => _MinMaxRes(Api.SW_SHOWMAXIMIZED, how); /// /// If maximized or minimized, makes normal (not min/max). /// Also unhides. /// /// What API to use: 0 ShowWindow, 1 SetWindowPlacement (no animation), 2 WM_SYSCOMMAND. /// The API call failed. No exception if the window did not obey. public void ShowNotMinMax(int how = 0) => _MinMaxRes(Api.SW_SHOWNORMAL, how); /// /// If minimized, restores previous non-minimized state (maximized or normal). /// Also unhides. /// /// What API to use: 0 ShowWindow, 1 SetWindowPlacement (no animation), 2 WM_SYSCOMMAND. /// The API call failed. No exception if the window did not obey. public void ShowNotMinimized(int how = 0) => _MinMaxRes(Api.SW_RESTORE, how); /// [Obsolete, EditorBrowsable(EditorBrowsableState.Never)] public void ShowMinimized(bool noAnimation) => _MinMaxRes(Api.SW_MINIMIZE, noAnimation ? 1 : 0); /// [Obsolete, EditorBrowsable(EditorBrowsableState.Never)] public void ShowMaximized(bool noAnimation) => _MinMaxRes(Api.SW_SHOWMAXIMIZED, noAnimation ? 1 : 0); /// [Obsolete, EditorBrowsable(EditorBrowsableState.Never)] public void ShowNotMinMax(bool noAnimation) => _MinMaxRes(Api.SW_SHOWNORMAL, noAnimation ? 1 : 0); /// [Obsolete, EditorBrowsable(EditorBrowsableState.Never)] public void ShowNotMinimized(bool noAnimation) => _MinMaxRes(Api.SW_RESTORE, noAnimation ? 1 : 0); /// /// Sets window min/max/normal/restore state. /// Also unhides. /// /// Must be SW_MINIMIZE, SW_RESTORE (restores to normal/max if minimized), SW_SHOWNORMAL or SW_SHOWMAXIMIZED. /// 0 ShowWindow, 1 SetWindowPlacement (no animation), 2 WM_SYSCOMMAND. /// void _MinMaxRes(int state, int how) { Debug.Assert(state == Api.SW_MINIMIZE || state == Api.SW_RESTORE || state == Api.SW_SHOWNORMAL || state == Api.SW_SHOWMAXIMIZED); ThrowIfInvalid(); bool ok = false, wasMinimized = IsMinimized; switch (state) { case Api.SW_MINIMIZE: ok = wasMinimized; break; case Api.SW_RESTORE: ok = !wasMinimized; break; case Api.SW_SHOWMAXIMIZED: ok = IsMaximized; break; default: //SW_SHOWNORMAL ok = !wasMinimized && !IsMaximized; //info: if invalid handle, Show() will return false, don't need to check here. break; } if (ok) { if (IsVisible) return; Show(true); } else { //WPF bug: if using SizeToContent, maximizing with ShowWindow/SetWindowPlacement sets incorrect size. // Example: if SizeToContent=vertical, resizes vertically for content, and horizontally for entire screen (not work area). // If inactive, resizes vertically for content, and horizontally does not resize. // Works well with WM_SYSCOMMAND. //if(how!=2 && state == Api.SW_SHOWMAXIMIZED && ClassNameIs("HwndWrapper[*")) how=2; //no. Never mind. if (how == 0) { Api.ShowWindow(this, state); //note: The API returns TRUE if was visible, not if succeeded. Tested: lastError can't be used. ok = _IsState(this, state); } else if (how == 1 && (ok = GetWindowPlacement_(out var p, false))) { int state2 = state; switch (state) { case Api.SW_MINIMIZE: if (p.showCmd == Api.SW_SHOWMAXIMIZED) p.flags |= Api.WPF_RESTORETOMAXIMIZED; else p.flags &= ~Api.WPF_RESTORETOMAXIMIZED; //Windows forgets to remove the flag break; case Api.SW_RESTORE: if ((p.showCmd == Api.SW_SHOWMINIMIZED) && (p.flags & Api.WPF_RESTORETOMAXIMIZED) != 0) state2 = Api.SW_SHOWMAXIMIZED; //without this would make normal break; } //if(wasMinimized) p.flags|=Api.WPF_ASYNCWINDOWPLACEMENT; //Windows bug: if window of another thread, deactivates currently active window and does not activate this window. However then animates window. If we set this while the window is not minimized, it would set blinking caret in inactive window. Instead we use another workaround, see below. p.showCmd = state2; ok = SetWindowPlacement_(ref p, false); } static bool _IsState(wnd w, int state) => state switch { Api.SW_MINIMIZE => w.IsMinimized, Api.SW_RESTORE => !w.IsMinimized, Api.SW_SHOWMAXIMIZED => w.IsMaximized, _ => !(w.IsMinimized || w.IsMaximized) } && w.IsVisible; if (!ok) { if (how == 2 || UacAccessDenied) { var cmd = state switch { Api.SW_MINIMIZE => Api.SC_MINIMIZE, Api.SW_SHOWMAXIMIZED => Api.SC_MAXIMIZE, //fails, never mind _ => Api.SC_RESTORE, }; Send(Api.WM_SYSCOMMAND, cmd); //if was minimized, now can be maximized, need to restore if SW_SHOWNORMAL if (state == Api.SW_SHOWNORMAL && IsMaximized) Send(Api.WM_SYSCOMMAND, cmd); //note: the Send return value is unreliable ok = _IsState(this, state); } if (!ok) ThrowNoNative("*minimize/maximize/restore*"); } if (!IsOfThisThread) { if (wasMinimized) ActivateL(); //Windows bug: if window of another thread, deactivates currently active window and does not activate this window else if (state == Api.SW_MINIMIZE) WndUtil.WaitForAnActiveWindow(); } } if (how != 1) MinimalSleepIfOtherThread_(); } /// /// Initializes a WINDOWPLACEMENT struct and calls API GetWindowPlacement. /// /// /// Remove workarea thickness from wp.rcNormalPosition. /// If not null, throws it if failed. /// Supports . /// Failed. Throws, only if errStr!=null, else returns false. internal bool GetWindowPlacement_(out Api.WINDOWPLACEMENT wp, bool rectInScreen, string errStr = null) { //initially this was public, but probably don't need. wp = new Api.WINDOWPLACEMENT(); wp.length = Api.SizeOf(wp); if (Api.GetWindowPlacement(this, ref wp)) { if (rectInScreen) _Wp1(ref wp, false); return true; } if (errStr != null) ThrowUseNative(errStr); return false; } /// /// Sets WINDOWPLACEMENT length field and calls API SetWindowPlacement. /// /// /// wp.rcNormalPosition is without workarea thickness. /// If not null, throws it if failed. /// Failed. Throws, only if errStr!=null, else returns false. internal bool SetWindowPlacement_(ref Api.WINDOWPLACEMENT wp, bool rectInScreen, string errStr = null) { //initially this was public, but probably don't need. if (rectInScreen) _Wp1(ref wp, true); wp.length = Api.SizeOf(wp); if (Api.SetWindowPlacement(this, wp)) return true; if (errStr != null) ThrowUseNative(errStr); return false; } void _Wp1(ref Api.WINDOWPLACEMENT wp, bool set) { if (!IsToolWindow) { var s = set ? screen.of(wp.rcNormalPosition) : screen.of(this); var v = s.Info; int i = set ? -1 : 1; wp.rcNormalPosition.Offset((v.workArea.left - v.rect.left) * i, (v.workArea.top - v.rect.top) * i); } } #endregion #region activate, focus internal static partial class Internal_ { /// /// No exceptions. /// internal static bool EnableActivate(bool goingToActivateAWindow) { if (_EnableActivate_AllowSetFore()) return true; //not locked, or already successfully called ASF_Key _EnableActivate_SendKey(true); if (_EnableActivate_AllowSetFore()) return true; //First time fails if the foreground window is of higher IL. Then sending keys does not work. //_EnableActivate_MinRes() makes no active window... if (goingToActivateAWindow) { _EnableActivate_MinRes(); return _EnableActivate_AllowSetFore(); } var wFore = active; bool retry = false; g1: _EnableActivate_MinRes(); if (!_EnableActivate_AllowSetFore()) return false; if (!wFore.Is0 && !retry) { Api.SetForegroundWindow(wFore); if (!_EnableActivate_AllowSetFore()) { retry = true; goto g1; } //didn't notice this but let's be safer } return true; //Other possible ways to allow set foreground window: //1. Instead of key can use attachthreadinput. But it is less reliable, eg works first time only, and does not allow our process to activate later easily. Does not work if foreground window is higher IL. //2. Call allowsetforegroundwindow from a hook from the foreground process (or from the shell process, not tested). Too dirty. Need 2 native dlls (32/64-bit). Cannot inject if higher IL. //Other possible ways to set foreground window: //1. Create temp window, RegisterHotKey, SendInput, and call SetForegroundWindow on WM_HOTKEY. Does not work if foreground window is higher IL. //2. WM_SETHOTKEY. More info below. Could not make it work well in all cases. //3. IUIAutomationElement.SetFocus. Does not work if foreground window is higher IL. Slow. Briefly makes the taskbar button red. //tested: cannot disable the foreground lock timeout with SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT) when this process cannot activate windows. } /// /// Sends a key (VK_0 up). It allows to activate now. /// Later this process usually (but not always) can activate easily (without key etc). It works even with higher IL windows. /// Don't know why is this behavior. Tested on all OS. /// Does not work if the foreground process has higher UAC IL. /// Does not work if bad input desktop. /// static void _EnableActivate_SendKey(bool debugOut) { //if (debugOut) Debug_.Print("EnableActivate: need key"); //FUTURE: temporarily enable to see maybe there are too many calls var x = new Api.INPUTK(0, 128, Api.KEYEVENTF_KEYUP); Api.SendInput(&x, dontThrow: true); //info: works without waiting. } /// /// Creates a temporary minimized window and restores it. It activates the window and allows us to activate. /// Then sets "no active window" to prevent auto-activating another window when destroying the temporary window. /// static void _EnableActivate_MinRes() { //Debug_.Print("EnableActivate: need min/res"); Debug_.Print($"EnableActivate: need min/res, {miscInfo.isInputDesktop(false)} {miscInfo.isInputDesktop(true)}, {wnd.active}"); //CONSIDER: don't try if !miscInfo.isInputDesktop(true) wnd t = WndUtil.CreateWindow(WndUtil.WindowClassDWP_, null, WS.POPUP | WS.MINIMIZE | WS.VISIBLE, WSE.TOOLWINDOW); //info: When restoring, the window must be visible, or may not work. try { var wp = new Api.WINDOWPLACEMENT { showCmd = Api.SW_RESTORE }; t.SetWindowPlacement_(ref wp, false); //activates t; fast (no animation) _EnableActivate_SendKey(false); //makes so that later our process can always activate _EnableActivate_AllowSetFore(); Api.SetForegroundWindow(getwnd.root); //set no foreground window, or may activate the higher IL window (maybe does not activate, but winevents hook gets events, in random order). Other way would be to destroy our window later, but more difficult to implement. } finally { Api.DestroyWindow(t); } //Another way: // t.Send(Api.WM_SETHOTKEY, ...); // Api.SendInputKey(...) //Works if UAC allows SendInput. //WM_SETHOTKEY works only if visible. No wm_syscommand if inactive, but activates. Unlike registerhotkey, not blocked by UAC even without modifiers. Need more testing. //This does not work: t.Send(WM_SYSCOMMAND, SC_HOTKEY); } /// /// Calls Api.AllowSetForegroundWindow(Api.GetCurrentProcessId()). /// static bool _EnableActivate_AllowSetFore() { return Api.AllowSetForegroundWindow(Api.GetCurrentProcessId()); } internal static bool ActivateL(wnd w) { if (w.IsActiveOrNoActiveAndThisIsWndRoot_) return true; try { bool canAct = EnableActivate(true); if (!Api.SetForegroundWindow(w)) { if (!canAct || !w.IsAlive) return false; //It happens when foreground process called LockSetForegroundWindow. //Although AllowSetForegroundWindow returns true, SetForegroundWindow fails. //It happens only before this process sends keys. Eg after first _EnableActivate_SendKey this never happens again. //If it has higher IL (and this process is User), also need _EnableActivate_MinRes. _EnableActivate_SendKey(true); if (!Api.SetForegroundWindow(w)) { _EnableActivate_MinRes(); if (!Api.SetForegroundWindow(w)) return false; } } //Often after SetForegroundWindow there is no active window for several ms. Not if the window is of this thread. // https://devblogs.microsoft.com/oldnewthing/20161118-00/?p=94745 if (w == getwnd.root) return active.Is0; if (WndUtil.WaitForAnActiveWindow()) return true; w.SendTimeout(1000, out _, 0); return !active.Is0; } //catch(AuWndException) { return false; } catch { return false; } } [Flags] internal enum ActivateFlags { /// /// Don't call (ie caller ensures it is valid). /// NoThrowIfInvalid = 1, /// /// Don't try to get top-level window (ie caller ensures it's a top-level window, not control). /// NoGetWindow = 2, /// /// Don't activate if has WS_EX_NOACTIVATE style or is toolwindow without title bar, unless cloaked. /// Then just calls , which in most cases does not work (inactive window). /// IgnoreIfNoActivateStyleEtc = 4, /// /// Wait for window animations to end. Eg when switching Win10 desktops. /// ForScreenCapture = 8, } } /// /// Activates this window (brings to the foreground). /// The same as , but has some options. /// Returns false if does not activate because of flag IgnoreIfNoActivateStyleEtc. /// /// /// internal bool Activate_(Internal_.ActivateFlags flags) { if (!flags.Has(Internal_.ActivateFlags.NoThrowIfInvalid)) ThrowIfInvalid(); if (flags.Has(Internal_.ActivateFlags.NoGetWindow)) Debug.Assert(!IsChild); else { var w = Window; if (w != this) { if (!w.Is0) print.warning("Child windows can't be activated. You may want to call Focus() instead."); return w.Activate_((flags | Internal_.ActivateFlags.NoGetWindow) & ~Internal_.ActivateFlags.NoThrowIfInvalid); } } bool R, noAct = false, isMinimized = false, ofThisThread = IsOfThisThread; bool forScreenCapture = 0 != (flags & Internal_.ActivateFlags.ForScreenCapture); if (IsMinimized) { ShowNotMinimized(1); isMinimized = IsMinimized; if (forScreenCapture && !isMinimized && !ofThisThread) Thread.Sleep(250); //although we use noAnimation, in some cases still restores with animation } if (!IsVisible) Show(true); R = IsActiveOrNoActiveAndThisIsWndRoot_; if (!R) { if (0 != (flags & Internal_.ActivateFlags.IgnoreIfNoActivateStyleEtc)) { if (IsNoActivateStyle_() && !IsCloaked) { ZorderTop(); return false; //if cloaked, need to activate to uncloak } } for (int i = 0; i < 3; i++) { bool ok = Internal_.ActivateL(this); if (!ofThisThread) { MinimalSleepNoCheckThread_(); MinimalSleepNoCheckThread_(); } if (ok) { wnd f = active; if (f == this) R = true; else if (this == getwnd.root) R = f.Is0; //activating GetDesktopWindow makes "no active window" else { //forgive if the target app instead activated another window of same thread int tid = ThreadId; if (tid == 0) break; if (f.ThreadId == tid) { //at first try to recognize such known windows, to avoid the hard way if (isMinimized || (f.Get.Owner == this && Rect.NoArea)) { R = true; } else { R = Api.SetForegroundWindow(getwnd.root) && ActivateL() && active.ThreadId == tid; if (R && !ofThisThread) { MinimalSleepNoCheckThread_(); R = active.ThreadId == tid; } } //Example 1: //Excel creates a minimized window for each workbook opened in that excel process. //These windows just add taskbar buttons. Also it allows to find and activate workbooks. //When you activate such window, Excel instead activates its main window, where it displays all workbooks. //For this reason we would fail (not always, because this may be temporarily active). //Same with PowerPoint. Other Office apps no. //Example 2: //Inno Setup, SQLite Expert. They have a zero-size owner window that just adds taskbar button. } } if (R) break; } if (noAct) break; Thread.Sleep(30); } } if (R && !ofThisThread && this != getwnd.root) { //If we activate a window that is on an inactive Win10 desktop, its desktop becomes active. //Windows on inactive desktops are cloaked. They are uncloaked after ~15 ms. if (IsCloaked) { R = false; for (int i = 0; i < 50; i++) { Thread.Sleep(30); if (R = !IsCloaked) break; } if (R) { if (forScreenCapture) Thread.Sleep(800); //need minimum 600 for 'find image' functions, because of animation while switching Win10 desktops. MinimalSleepNoCheckThread_(); R = IsActive; if (!R && ActivateL()) { MinimalSleepNoCheckThread_(); R = IsActive; } } } } if (!R) { InputDesktopException.ThrowIfBadDesktop("*activate window", detectLocked: true); ThrowNoNative("*activate*"); } if (forScreenCapture) MinimalSleepIfOtherThread_(); return true; //tested: if the window is hung, activates the ghost window and fails (exception). It's OK. } internal bool IsNoActivateStyle_() { var es = ExStyle; if ((es & WSE.NOACTIVATE) != 0) return true; if ((es & (WSE.TOOLWINDOW | WSE.APPWINDOW)) == WSE.TOOLWINDOW) return !HasStyle(WS.CAPTION); return false; } /// /// Activates this window. Also makes it visible and not minimized. /// The active window is in the foreground and receives keyboard and mouse input. /// /// Self. /// /// Activating a window usually also uncloaks it, for example switches to its virtual desktop on Windows 10/11. /// Fails (throws exception) if cannot activate this window, except: /// - When this is a control. Then activates its top-level parent window. /// - When the window's process instead activates another window of the same thread. /// - If this is , just deactivates the currently active window. /// /// Failed to activate. /// /// /// /// /// public wnd Activate() { Activate_(0); return this; } //CONSIDER: if fails to activate: //dialog.show("Failed to activate window", w.ToString(), footer: The script will continue if you activate the window in {x} s.", timeout: 10); //rejected: Activate(double waitS = 0) /// Max time interval (seconds) to wait until the window naturally becomes active before activating it. // It would have sense if we somehow know that the window was created by previous action (or several actions). // Would need an explicit function call before the action(s). Nobody would use it. /// /// Lightweight version of . Does not throw exceptions. /// /// /// Call and handle exceptions; on exception return false. /// If false (default), just activates; does not show hidden, does not restore minimized, does not check whether it is a top-level window or control, does not check whether activated exactly this window, etc. /// /// false if failed. public bool ActivateL(bool full = false) { if (!full) return Internal_.ActivateL(this); try { Activate(); return true; } catch { return false; } } //Too unreliable. ///// ///// Calls API LockSetForegroundWindow, which temporarily prevents other applications from activating windows easily with SetForegroundWindow. ///// If LockSetForegroundWindow fails, calls EnableActivate and retries. ///// ///// Lock or unlock. //public static bool lockActiveWindow(bool on) //{ // uint f = on ? Api.LSFW_LOCK : Api.LSFW_UNLOCK; // if(Api.LockSetForegroundWindow(f)) return true; // return EnableActivate() && Api.LockSetForegroundWindow(f); //} /// /// Sets the keyboard input focus to this control. /// Also activates its top-level parent window (see ). /// /// /// The control can belong to any process/thread. With controls of this thread you can use the more lightweight function . /// Works not with all windows. For example, does not work with Windows Store apps. Then use . /// Can instead focus a child control. For example, if this is a combo box, it will focus its child Edit control. Then does not throw exception. /// This can be control or top-level window. Top-level windows also can have focus. /// Fails when the target process is admin or uiAccess and this process isn't. See [](xref:uac). /// /// /// - Invalid handle. /// - Disabled. /// - Failed to set focus. /// - Failed to activate parent window. /// /// /// /// /// public void Focus() { ThrowIfInvalid(); wnd wTL = Window; if (!wTL.IsActive) wTL.Activate_(Internal_.ActivateFlags.NoGetWindow); int tid = ThreadId; if (tid == Api.GetCurrentThreadId()) { if (!thisThread.focus(this)) { if (!IsEnabled(true)) goto gDisabled; goto gFailed; } return; } if (IsFocused) return; if (!IsEnabled(true)) goto gDisabled; bool ok = false; using (new AttachThreadInput_(tid, out bool atiOK)) { if (atiOK) { //FUTURE: if fails, try elm.Focus or UIA. AttachThreadInput is unreliable. for (int i = 0; i < 5; i++) { if (i > 0) Thread.Sleep(30); lastError.clear(); if (thisThread.focus(this)) { wnd f = focused; if (f == this || f.IsChildOf(this)) { ok = true; break; } } } } } if (!ok) goto gFailed; MinimalSleepNoCheckThread_(); return; gDisabled: //SetFocus fails if disabled ThrowIfInvalid(); ThrowNoNative("*set focus. Disabled"); gFailed: ThrowUseNative("*set focus"); } /// /// Gets the control or window that has the keyboard input focus. /// /// /// The control/window can belong to any process/thread. With controls/windows of this thread you can use the more lightweight function . /// Calls API GetGUIThreadInfo. /// /// /// public static wnd focused { get { miscInfo.getGUIThreadInfo(out var g); return g.hwndFocus; } } //FUTURE: need functions that wait eg max 1 s until a window is focused or active. // Example: after activating a window using the taskbar, there is no active window for 100 ms or more. // Example: after opening a common file dialog, the Edit control is focused after 200 ms, until that there is no focus. // For 'active' we already have More.WaitForAnActiveWindow. /// /// Returns true if this is the control or window that has the keyboard input focus. /// /// /// This control/window can belong to any process/thread. With controls/windows of this thread you can use the more lightweight function . /// Calls . /// /// public bool IsFocused => !this.Is0 && this == focused; /// /// Functions that can be used only with windows/controls of this thread. /// public static class thisThread { /// /// Calls API SetFocus. It sets the keyboard input focus to the specified control or window, which must be of this thread. /// /// false if failed. Supports . /// /// Fails if the control/window belongs to another thread or is invalid or disabled. /// Can instead focus a child control. For example, if combo box, will focus its child Edit control. Then returns true. /// public static bool focus(wnd w) { if (w.Is0) { Api.SetLastError(Api.ERROR_INVALID_WINDOW_HANDLE); return false; } var f = Api.GetFocus(); if (f == w) return true; if (!Api.SetFocus(w).Is0) return true; if (f.Is0) return !Api.GetFocus().Is0; return false; } /// /// Gets the focused control or window of this thread. /// /// /// Calls API GetFocus. /// public static wnd focused => Api.GetFocus(); //rejected. ///// ///// Gets the focused control or form of this thread. ///// ///// ///// Calls API GetFocus and . ///// //public static System.Windows.Forms.Control focusedWinformsControl => System.Windows.Forms.Control.FromHandle(Api.GetFocus().Handle); /// /// Returns true if w is the focused control or window of this thread. /// /// /// Calls API GetFocus. /// public static bool isFocused(wnd w) => !w.Is0 && w == Api.GetFocus(); /// /// Gets the active window of this thread. /// Calls API GetActiveWindow. /// public static wnd active => Api.GetActiveWindow(); } #endregion #region rect /// /// Gets rectangle (position and size) in screen coordinates. /// /// Receives the rectangle. Will be default(RECT) if failed. /// Don't include the transparent part of window border. For it is used API DwmGetWindowAttribute(DWMWA_EXTENDED_FRAME_BOUNDS); it is less reliable. /// /// The same as the property. /// Calls API GetWindowRect and returns its return value. /// Supports . /// public bool GetRect(out RECT r, bool withoutExtendedFrame = false) { if (withoutExtendedFrame) { RECT t; if (0 == Api.DwmGetWindowAttribute(this, Api.DWMWA.EXTENDED_FRAME_BOUNDS, &t, 16)) { r = t; return true; //tested: on Win7 gets physical (not logical) coords of DPI-scaled window } } if (Api.GetWindowRect(this, out r)) return true; r = default; return false; } /// /// Gets rectangle (position and size) in screen coordinates. /// /// /// Calls . Returns default(RECT) if fails (eg window closed). /// Supports . /// public RECT Rect { get { GetRect(out RECT r); return r; } } ///// ///// Gets width and height. ///// ///// Receives width and height. Will be default(SIZE) if failed. ///// ///// The same as the property. ///// Calls API GetWindowRect and returns its return value. ///// Supports . ///// //public bool GetSize(out SIZE z) { // if (Api.GetWindowRect(this, out RECT r)) { z = new SIZE(r.Width, r.Height); return true; } // z = default; // return false; //} ///// ///// Gets width and height. ///// ///// ///// Calls . Returns default(SIZE) if fails (eg window closed). ///// Supports . ///// //public SIZE Size { // get { // GetSize(out SIZE z); // return z; // } //} ///// ///// Gets position in screen coordinates. ///// ///// ///// Calls . Returns default(POINT) if fails (eg window closed). ///// Supports . ///// //public POINT XY { // get { var r = Rect; return new(r.left, r.top); } //} ///// ///// Gets horizontal position in screen coordinates. ///// ///// Calls . //public int X { // get => Rect.left; //} ///// ///// Gets vertical position in screen coordinates. ///// ///// Calls . //public int Y { // get => Rect.top; //} ///// ///// Gets width. ///// ///// Calls . //public int Width { // get => Rect.Width; //} ///// ///// Gets height. ///// ///// Calls . //public int Height { // get => Rect.Height; //} /// /// Gets client area rectangle. /// /// Receives the rectangle. Will be default(RECT) if failed. /// /// Get rectangle in screen coordinates; like but faster. /// If false (default), calls API GetClientRect; the same as . /// /// Supports . /// public bool GetClientRect(out RECT r, bool inScreen = false) { #if true if (Api.GetClientRect(this, out r) && (!inScreen || Api.MapWindowPoints(this, default, ref r, out _))) return true; r = default; return false; #else //>= 2 times slower if(inScreen) return GetWindowAndClientRectInScreen(out _, out r); if(Api.GetClientRect(this, out r)) return true; r = default; return false; #endif } /// /// Gets client area rectangle (width and height). /// /// /// The left and top fields are always 0. /// Calls . Returns default(RECT) if fails (eg window closed). /// public RECT ClientRect { get { GetClientRect(out RECT r); return r; } } ///// ///// Gets client area width and height. ///// ///// Receives width and height. Will be default(RECT) if failed. ///// ///// The same as the property. ///// The same as , just the parameter type is different. ///// Calls API GetClientRect and returns its return value. ///// Supports . ///// //public bool GetClientSize(out SIZE z) { // if (Api.GetClientRect(this, out RECT r)) { z = new SIZE(r.right, r.bottom); return true; } // z = default; // return false; //} /// /// Gets client area rectangle (width and height) in screen. /// /// /// Calls . Returns default(RECT) if fails (eg window closed). /// public RECT ClientRectInScreen { get { GetClientRect(out RECT r, true); return r; } } ///// ///// Gets client area width and height. ///// ///// ///// The same as , just the return type is different. ///// Calls . Returns default(SIZE) if fails (eg window closed). ///// //public SIZE ClientSize { // get { // GetClientSize(out SIZE z); // return z; // } //} ///// ///// Gets client area width. ///// ///// Calls . //public int ClientWidth { // get => ClientSize.width; //} ///// ///// Gets client area height. ///// ///// Calls . //public int ClientHeight { // get => ClientSize.height; //} /// /// Resizes this window to match the specified client area size. /// Calls . /// /// Client area width. Use null to not change. /// Client area height. Use null to not change. /// public void ResizeClient(int? width, int? height) { if (GetWindowInfo_(out var u)) { int W = width ?? u.rcClient.Width; W += u.rcWindow.Width - u.rcClient.Width; int H = height ?? u.rcClient.Height; H += u.rcWindow.Height - u.rcClient.Height; if (ResizeL(W, H)) return; } ThrowUseNative(); } /// /// Calls API GetWindowInfo. /// /// Receives window/client rectangles, styles etc. /// Supports . internal bool GetWindowInfo_(out Api.WINDOWINFO wi) { //initially this was public, but probably don't need. wi = default; wi.cbSize = Api.SizeOf(wi); return Api.GetWindowInfo(this, ref wi); } /// /// Gets window rectangle and client area rectangle, both in screen coordinates. /// /// Receives window rectangle. /// Receives client area rectangle. /// Calls API GetWindowInfo. Supports . public bool GetWindowAndClientRectInScreen(out RECT rWindow, out RECT rClient) { if (GetWindowInfo_(out var u)) { rWindow = u.rcWindow; rClient = u.rcClient; return true; } rWindow = default; rClient = default; return false; } //rejected: because now GetClientRect has 'inScreen' parameter. ///// ///// Gets client area rectangle in screen coordinates. ///// ///// ///// Calls . Returns default(RECT) if fails (eg window closed). ///// //public RECT ClientRectInScreen //{ // get // { // GetWindowAndClientRectInScreen(out var rw, out var rc); // return rc; // } //} /// /// Converts coordinates relative to the client area of this window to coordinates relative to the client area of window w. /// /// Supports . public bool MapClientToClientOf(wnd w, ref RECT r) => !Is0 && !w.Is0 && Api.MapWindowPoints(this, w, ref r, out _); /// /// Converts coordinates relative to the client area of this window to coordinates relative to the client area of window w. /// /// Supports . public bool MapClientToClientOf(wnd w, ref POINT p) => !Is0 && !w.Is0 && Api.MapWindowPoints(this, w, ref p, out _); /// /// Converts coordinates relative to the client area of this window to coordinates relative to the screen. /// /// Supports . public bool MapClientToScreen(ref RECT r) => !Is0 && Api.MapWindowPoints(this, default, ref r, out _); /// /// Converts coordinates relative to the client area of this window to coordinates relative to the screen. /// /// Supports . public bool MapClientToScreen(ref POINT p) => Api.ClientToScreen(this, ref p); /// /// Converts coordinates relative to the screen to coordinates relative to the client area of this window. /// /// Supports . public bool MapScreenToClient(ref RECT r) => !Is0 && Api.MapWindowPoints(default, this, ref r, out _); /// /// Converts coordinates relative to the screen to coordinates relative to the client area of this window. /// /// Supports . public bool MapScreenToClient(ref POINT p) => Api.ScreenToClient(this, ref p); /// /// Converts coordinates relative to the client area of this window to coordinates relative to the top-left corner of this window. /// /// Supports . public bool MapClientToWindow(ref POINT p) { if (!GetWindowAndClientRectInScreen(out var rw, out var rc)) return false; p.x += rc.left - rw.left; p.y += rc.top - rw.top; return true; } /// /// Converts coordinates relative to the client area of this window to coordinates relative to the top-left corner of this window. /// /// Supports . public bool MapClientToWindow(ref RECT r) { if (!GetWindowAndClientRectInScreen(out var rw, out var rc)) return false; r.Offset(rc.left - rw.left, rc.top - rw.top); return true; } /// /// Converts coordinates relative to the top-left corner of this window to coordinates relative to the client area of this window. /// /// Supports . public bool MapWindowToClient(ref POINT p) { if (!GetWindowAndClientRectInScreen(out var rw, out var rc)) return false; p.x += rw.left - rc.left; p.y += rw.top - rc.top; return true; } /// /// Converts coordinates relative to the top-left corner of this window to coordinates relative to the client area of this window. /// /// Supports . public bool MapWindowToClient(ref RECT r) { if (!GetWindowAndClientRectInScreen(out var rw, out var rc)) return false; r.Offset(rw.left - rc.left, rw.top - rc.top); return true; } /// /// Converts coordinates relative to the top-left corner of this window to screen coordinates. /// /// Supports . public bool MapWindowToScreen(ref POINT p) { if (!GetRect(out var rw)) return false; p.x += rw.left; p.y += rw.top; return true; } /// /// Converts coordinates relative to the top-left corner of this window to screen coordinates. /// /// Supports . public bool MapWindowToScreen(ref RECT r) { if (!GetRect(out var rw)) return false; r.Offset(rw.left, rw.top); return true; } /// /// Gets rectangle of this window (usually control) relative to the client area of another window (usually parent). /// /// Parent, ancestor or any other window or control. If default(wnd), gets rectangle in screen. /// Receives the rectangle. /// Supports . /// /// public bool GetRectIn(wnd w, out RECT r) { if (w.Is0) return GetRect(out r); return GetRect(out r) && w.MapScreenToClient(ref r); } /// /// Gets child window rectangle in the client area of the direct parent window. /// /// /// Calls and . Returns default(RECT) if fails (eg window closed). /// public RECT RectInDirectParent => GetRectIn(Get.DirectParent, out var r) ? r : default; /// /// Gets child window rectangle in the client area of the top-level parent window. /// /// /// Calls and . Returns default(RECT) if fails (eg window closed). /// public RECT RectInWindow => GetRectIn(Window, out var r) ? r : default; /// /// Gets rectangle of normal (restored) window even if currently it is minimized or maximized. /// /// Supports . public bool GetRectNotMinMax(out RECT r) { if (!(IsMinimized || IsMaximized)) return GetRect(out r); if (!GetWindowPlacement_(out var p, true)) { r = default; return false; } r = p.rcNormalPosition; return true; } //TEST: undocumented API GetWindowMinimizeRect. Gets rect of taskbar button or where it would be minimized above the Start button. But probably not useful. /// /// Returns mouse pointer position relative to the client area of this window. /// public POINT MouseClientXY { get { Api.GetCursorPos(out var p); if (!MapScreenToClient(ref p)) p = default; return p; } } /// /// Returns true if this window (its rectangle) contains the specified point. /// /// X coordinate in screen. Not used if default. Examples: 10, ^10 (reverse), .5f (fraction). /// Y coordinate in screen. Not used if default. public bool ContainsScreenXY(Coord x, Coord y) { POINT p = Coord.Normalize(x, y); if (!GetRect(out RECT r)) return false; if (!r.Contains(x.IsEmpty ? r.left : p.x, y.IsEmpty ? r.top : p.y)) return false; return true; //note: we don't use name ContainsXY and 2 overloads, mostly because of possible incorrect usage. Also now easier to read the code. } /// /// Returns true if this control (its rectangle) contains the specified point in parent window. /// /// /// Direct or indirect parent window. The coordinates are relative to its client area. /// Actually this and parent can be any windows or controls, the function does not check whether this is a child of parent. /// /// X coordinate. Not used if default. Examples: 10, ^10 (reverse), .5f (fraction). /// Y coordinate. Not used if default. public bool ContainsWindowXY(wnd parent, Coord x, Coord y) { if (!parent.IsAlive) return false; POINT p = Coord.NormalizeInWindow(x, y, parent); if (!GetRectIn(parent, out RECT r)) return false; if (!r.Contains(x.IsEmpty ? r.left : p.x, y.IsEmpty ? r.top : p.y)) return false; return true; } /// /// This overload calls with , x and y. /// /// public bool ContainsWindowXY(Coord x, Coord y) { return ContainsWindowXY(Window, x, y); } #endregion #region move, resize, SetWindowPos /// /// Calls API SetWindowPos. /// /// /// /// /// /// /// A window or a constant from (TOP, BOTTOM, TOPMOST, NOTOPMOST). /// /// Supports . /// public bool SetWindowPos(SWPFlags swpFlags, int x = 0, int y = 0, int cx = 0, int cy = 0, wnd zorderAfter = default) { return Api.SetWindowPos(this, zorderAfter, x, y, cx, cy, swpFlags); } /// /// Moves and resizes. /// /// /// See also . It is better to use in automation scripts, with windows of any process/thread. It throws exceptions, supports optional/reverse/fractional/workarea coordinates, restores if min/max, does not support SWP flags. /// This function is more lightweight, it just calls API SetWindowPos with flags NOZORDER|NOOWNERZORDER|NOACTIVATE|swpFlagsToAdd. It is better to use in programming, with windows of current thread. /// Supports . /// /// For top-level windows use screen coordinates. For controls - direct parent client coordinates. /// /// public bool MoveL(int x, int y, int width, int height, SWPFlags swpFlagsToAdd = 0) { return SetWindowPos(SWPFlags.NOZORDER | SWPFlags.NOOWNERZORDER | SWPFlags.NOACTIVATE | swpFlagsToAdd, x, y, width, height); } /// /// Moves and resizes. Same as . /// /// Use the visible rectangle without the transparent frame. Note: the window should be visible. This parameter not used with child windows. public bool MoveL(RECT r, SWPFlags swpFlagsToAdd = 0, bool visibleRect = false) { if (visibleRect && GetRect(out var rTrue) && GetRect(out var rVisible, withoutExtendedFrame: true) && rVisible != rTrue) { r.left -= rVisible.left - rTrue.left; r.top -= rVisible.top - rTrue.top; r.right += rTrue.right - rVisible.right; r.bottom += rTrue.bottom - rVisible.bottom; } return MoveL(r.left, r.top, r.Width, r.Height, swpFlagsToAdd); } /// /// Moves. /// /// /// See also . It is better to use in automation scripts, with windows of any process/thread. It throws exceptions, supports optional/reverse/fractional/workarea coordinates, restores if min/max. /// This function is more lightweight, it just calls API SetWindowPos with flags NOSIZE|NOZORDER|NOOWNERZORDER|NOACTIVATE. It is better to use in programming, with windows of current thread. /// Supports . /// /// For top-level windows use screen coordinates. For controls - direct parent client coordinates. /// /// public bool MoveL(int x, int y) { return MoveL(x, y, 0, 0, SWPFlags.NOSIZE); } internal bool MoveL_(POINT p) => MoveL(p.x, p.y); /// /// Resizes. /// /// /// See also . It is better to use in automation scripts, with windows of any process/thread. It throws exceptions, supports optional/reverse/fractional/workarea coordinates, restores if min/max. /// This function is more lightweight, it just calls API SetWindowPos with flags NOMOVE|NOZORDER|NOOWNERZORDER|NOACTIVATE. It is better to use in programming, with windows of current thread. /// Supports . /// /// public bool ResizeL(int width, int height) { return MoveL(0, 0, width, height, SWPFlags.NOMOVE); } internal bool ResizeL_(SIZE z) => ResizeL(z.width, z.height); /// /// Moves and/or resizes. /// /// Left. If default, does not move in X axis. Examples: 10, ^10 (reverse), .5f (fraction). /// Top. If default, does not move in Y axis. /// Width. If default, does not change width. /// Height. If default, does not change height. /// x y width height are relative to the work area. This parameter not used with child windows. /// x y width height are relative to this screen or its work area. Default - primary. Example: screen.index(1). This parameter not used with child windows. /// Use the visible rectangle without the transparent frame. Note: the window should be visible. This parameter not used with child windows. /// /// /// Also restores the visible top-level window if it is minimized or maximized. /// For top-level windows use screen coordinates. For controls - direct parent client area coordinates. /// With windows of current thread usually it's better to use . /// public void Move(Coord x, Coord y, Coord width, Coord height, bool workArea = false, screen screen = default, bool visibleRect = false) { ThrowIfInvalid(); wnd w = Get.DirectParent; POINT xy, wh; if (!w.Is0) { xy = Coord.NormalizeInWindow(x, y, w); wh = Coord.NormalizeInWindow(width, height, w); } else { xy = Coord.Normalize(x, y, workArea, screen); wh = Coord.Normalize(width, height, workArea, screen, widthHeight: true); } SWPFlags f = 0; uint getRect = 0; if (x.IsEmpty && y.IsEmpty) f |= SWPFlags.NOMOVE; else if (x.IsEmpty) getRect |= 1; else if (y.IsEmpty) getRect |= 2; if (width.IsEmpty && height.IsEmpty) f |= SWPFlags.NOSIZE; else if (width.IsEmpty) getRect |= 4; else if (height.IsEmpty) getRect |= 8; if (getRect != 0) { if (!GetRectIn(w, out RECT r)) ThrowUseNative("*move/resize*"); if ((getRect & 1) != 0) xy.x = r.left; if ((getRect & 2) != 0) xy.y = r.top; if ((getRect & 4) != 0) wh.x = r.Width; if ((getRect & 8) != 0) wh.y = r.Height; } //restore min/max, except if child or hidden if (w.Is0 && (IsMinimized || IsMaximized) && IsVisible) { ShowNotMinMax(1); //info: '&& IsVisible' because ShowNotMinMax unhides } if (visibleRect && GetRect(out var rTrue) && GetRect(out var rVisible, withoutExtendedFrame: true) && rVisible != rTrue) { int dx = rVisible.left - rTrue.left, dy = rVisible.top - rTrue.top; if (!x.IsEmpty) xy.x -= dx; if (!y.IsEmpty) xy.y -= dy; if (!width.IsEmpty) wh.x += rTrue.right - rVisible.right + dx; if (!height.IsEmpty) wh.y += rTrue.bottom - rVisible.bottom + dy; } if (!MoveL(xy.x, xy.y, wh.x, wh.y, f)) ThrowUseNative("*move/resize*"); MinimalSleepIfOtherThread_(); } /// /// Moves. /// /// x y are relative to the work area. This parameter not used with child windows. /// x y are relative to this screen or its work area. Default - primary. Example: screen.index(1). This parameter not used with child windows. /// public void Move(Coord x, Coord y, bool workArea = false, screen screen = default, bool visibleRect = false) { Move(x, y, default, default, workArea, screen, visibleRect); } /// /// Resizes. /// /// Width. If default, does not change width. /// Height. If default, does not change height. /// For etc use width/height of the work area. This parameter not used with child windows. /// For etc use width/height of this screen. Default - primary. Example: screen.index(1). This parameter not used with child windows. /// Use the visible rectangle without the transparent frame. Note: the window should be visible. This parameter not used with child windows. /// /// /// Also restores the visible top-level window if it is minimized or maximized. /// With windows of current thread usually it's better to use . /// public void Resize(Coord width, Coord height, bool workArea = false, screen screen = default, bool visibleRect = false) { Move(default, default, width, height, workArea, screen, visibleRect); } #endregion #region MoveInScreen, EnsureInScreen, Screen internal static partial class Internal_ { static void _MoveRect(ref RECT r, Coord left, Coord top, RECT rs, bool ensureIn, bool ensureMethod) { int x, y; if (ensureMethod) { Debug.Assert(ensureIn && left.IsEmpty && top.IsEmpty); //left/top unused x = r.left; y = r.top; } else { if (left.IsEmpty) left = Coord.Center; if (top.IsEmpty) top = Coord.Center; (x, y) = Coord.NormalizeInRect(left, top, rs); switch (left.Type) { case CoordType.Reverse: x -= r.Width; break; case CoordType.Fraction: x -= (int)(r.Width * left.FractionValue); break; } switch (top.Type) { case CoordType.Reverse: y -= r.Height; break; case CoordType.Fraction: y -= (int)(r.Height * top.FractionValue); break; } } if (ensureIn) { x = Math.Max(Math.Min(x, rs.right - r.Width), rs.left); y = Math.Max(Math.Min(y, rs.bottom - r.Height), rs.top); if (r.Width > rs.Width) r.Width = rs.Width; if (r.Height > rs.Height) r.Height = rs.Height; } r.Move(x, y); } public static void MoveRectInRect(ref RECT r, Coord left, Coord top, RECT rs, bool ensureIn) { _MoveRect(ref r, left, top, rs, ensureIn, false); } public static void MoveRectInScreen(bool ensureMethod, ref RECT r, Coord left, Coord top, screen screen, bool workArea, bool ensureIn) { var scr2 = !screen.IsEmpty ? screen.Now : ensureMethod ? screen.of(r) : screen.primary; var rs = scr2.GetRect(workArea); _MoveRect(ref r, left, top, rs, ensureIn, ensureMethod); } /// /// Moves w to left top in screen, and/or ensures it's in screen. /// /// Just ensure in screen. Not used parameters: left, top. /// /// If empty, uses center. /// If empty, uses center. /// If empty, uses screen of w. /// /// public static void MoveWindowInScreen(bool ensureMethod, wnd w, Coord left, Coord top, screen screen, bool workArea, bool ensureIn) { var scr2 = !screen.IsEmpty ? screen.Now : screen.of(w); var rs = scr2.GetRect(workArea); var scr1 = screen.of(w); bool moveToOtherScreen = scr2 != scr1; bool isMaximized = w.IsMaximized, isMinimized = !isMaximized && w.IsMinimized; if (isMaximized || isMinimized) { bool visible = w.IsVisible; if (moveToOtherScreen) { bool restoreMaximized = false; if (isMinimized && w.GetWindowPlacement_(out var wp1, false, "*move*") && 0 != (wp1.flags & 2)) { wp1.flags &= ~2u; restoreMaximized = w.SetWindowPlacement_(ref wp1, false, "*move*"); } Api.ShowWindow(w, Api.SW_SHOWNOACTIVATE); //less flickering than with SetWindowPlacement; same speed _Move(); Api.ShowWindow(w, isMaximized ? Api.SW_SHOWMAXIMIZED : Api.SW_SHOWMINNOACTIVE); if (restoreMaximized && w.GetWindowPlacement_(out var wp2, false)) { wp2.flags |= 2; w.SetWindowPlacement_(ref wp2, false); } if (!visible) w.ShowL(false); //With SetWindowPlacement could avoid restoring and showing, but then does not resize for different DPI. //Also tested: temporarily remove WS_MAXIMIZE. With some windows does not work well, eg Firefox. Does not work with minimized. Dangerous, undocumented. //never mind: old Dreamweaver maximizes to the old screen. Need to repeat or before maximizing wait ~300 ms. } else { w.GetWindowPlacement_(out var wp, true, "*move*"); _MoveRect(ref wp.rcNormalPosition, left, top, rs, ensureIn, ensureMethod); wp.showCmd = visible ? Api.SW_SHOWNA : Api.SW_HIDE; w.SetWindowPlacement_(ref wp, true, "*move*"); } //would need this if using SetWindowPlacement when moving to other screen //if (isMaximized && moveToOtherScreen) { // //When moved to screen's coordinates and sized to screen's work area size, OS adjusts window pos to be correct, ie border is outside screen, but invisible in adjacent screen. // //Must call SetWindowPos twice, or it may refuse to move at all. // //OS does this on Win+Shift+Left or Right. // //Some windows does not respond well. Eg old Dreamweaver. // //The biggest problem is DPI-scaling. Even the OS hotkey does not scale correctly (because the app does not have a chance to scale itself). // rs = scr2.WorkArea; // if (!w.MoveL(rs.left, rs.top) || !w.ResizeL(rs.Width, rs.Height)) w.ThrowUseNative("*move*"); //} } else { _Move(); } void _Move() { if (moveToOtherScreen) { int dpi1 = scr1.Dpi, dpi2 = scr2.Dpi; if (dpi2 != dpi1 || !ensureIn) { //resize w if would be too big for that screen. Because DPI may change if bigger part will be in another screen. int wid = rs.Width, hei = rs.Height; if (dpi2 != dpi1) { wid = Math2.MulDiv(wid, dpi1, dpi2); hei = Math2.MulDiv(hei, dpi1, dpi2); } var k = w.Rect; if (k.Width > wid || k.Height > hei) { w.ResizeL(Math.Min(k.Width, wid), Math.Min(k.Height, hei)); } } if (dpi2 != dpi1) { //at first move to the screen, let it DPI-scale self. Some windows don't DPI-scale self when moving with resizing. Anyway would need to correct afterwards. w.MoveL(rs.left, rs.top); } } var r = w.Rect; _MoveRect(ref r, left, top, rs, ensureIn, ensureMethod); w.MoveL(r); } w.MinimalSleepIfOtherThread_(); } } /// /// Moves this window to coordinates x y in specified screen, and ensures that entire window is in that screen. /// /// X coordinate in the specified screen. If default - screen center. Examples: 10, ^10 (reverse), .5f (fraction). /// Y coordinate in the specified screen. If default - screen center. /// Move to this screen (see ). If default, uses screen of this window. Example: screen.index(1). /// Use the work area, not whole screen. Default true. /// If part of window is not in screen, move and/or resize it so that entire window would be in screen. Default true. /// /// /// If the window is maximized, minimized or hidden, it will have the new position and size when restored, not immediately, except when moving maximized to another screen. /// /// public void MoveInScreen(Coord x, Coord y, screen screen = default, bool workArea = true, bool ensureInScreen = true) { Internal_.MoveWindowInScreen(false, this, x, y, screen, workArea, ensureInScreen); } /// /// Moves this window if need, to ensure that entire window is in that screen. /// /// Move to this screen (see ). If default, uses screen of this window. /// Use the work area, not whole screen. Default true. /// /// /// If the window is maximized, minimized or hidden, it will have the new position and size when restored, not immediately. /// /// public void EnsureInScreen(screen screen = default, bool workArea = true) { Internal_.MoveWindowInScreen(true, this, default, default, screen, workArea, true); } /// /// Moves this window to the center of the screen. Ensures that entire window is in that screen. /// /// Move to this screen (see ). If default, uses screen of this window. /// /// Calls ShowNotMinMax(true) and MoveInScreen(default, default, screen, true). See . /// public void MoveToScreenCenter(screen screen = default) { ShowNotMinMax(); MoveInScreen(default, default, screen, true); } /// /// Gets of the screen that contains this window (the biggest part of it) or is nearest to it. /// If this window handle is default(wnd) or invalid, gets the primary screen. /// Calls . /// public screen Screen => screen.of(this); #endregion #region Zorder const SWPFlags _SWP_ZORDER_OWNER = SWPFlags.NOMOVE | SWPFlags.NOSIZE | SWPFlags.NOACTIVATE, _SWP_ZORDER_NOOWNER = SWPFlags.NOMOVE | SWPFlags.NOSIZE | SWPFlags.NOACTIVATE | SWPFlags.NOOWNERZORDER; //info: with SWP_NOOWNERZORDER the API does not zorder owner and owned windows. //note: without SWP_NOOWNERZORDER the API is full of bugs. Eg fails if the window has a topmost owned window. /// /// Calls SetWindowPos with zorder-only flags. /// Does not check owners etc. /// /// /// Call wAfter = wAfter.Get.Previous();. /// With SWP_NOOWNERZORDER. /// internal bool ZorderL_(wnd wAfter, bool before = false, bool noownerzorder = false) { if (before) { var w0 = wAfter; wAfter = wAfter.Get.Previous(); if (wAfter == this) return true; if (wAfter.Is0 && !w0.IsAlive) return false; } return SetWindowPos(noownerzorder ? _SWP_ZORDER_NOOWNER : _SWP_ZORDER_OWNER, zorderAfter: wAfter); } /// /// Places this window above window w in the Z order. /// /// Another window. /// Place owner windows below this window. If false (default), does it only if they otherwise would be on top of this window. /// /// This window and w can be both top-level windows or both controls of same parent. /// Can make this window topmost or non-topmost, depending on where w is in the Z order. /// Also affects owned and owner windows, but does not make them topmost/non-topmost if not necessary. /// Uses API SetWindowPos. /// Supports . /// public bool ZorderAbove(wnd w, bool ownerToo = false) => _ZorderAB(w, true, ownerToo); /// /// Places this window below window w in the Z order. /// /// Another window. If default(wnd), calls . /// public bool ZorderBelow(wnd w, bool ownerToo = false) => _ZorderAB(w, false, ownerToo); bool _ZorderAB(wnd w, bool before, bool ownerToo, bool top = false) { if (!top) { if (before || !w.Is0) { if (!w.IsAlive || w == this) return false; if (before) { g1: var wp = w.Get.Previous(); if (wp.Is0 && !w.IsAlive) return false; //w is at the very top? w = wp; for (wnd ow = Get.Owner; !ow.Is0; ow = ow.Get.Owner) if (w == ow) goto g1; } } } if (IsChild) return SetWindowPos(_SWP_ZORDER_NOOWNER, zorderAfter: w); //All ZorderX functions work almost like SetWindowPos without SWP_NOOWNERZORDER should work as documented. // However cannot simply call it because of its bugs. // To simulate it, we zorder this and each owned/owner window with SWP_NOOWNERZORDER. // The flag isn't well documented. With it zorders only this window; ignores owner and owned windows. // Speed: 10% slower than SetWindowPos without SWP_NOOWNERZORDER. // Still can't overcome this API bug: can't zorder above a topmost uiAccess window; also Start menu. var od = new getwnd.OwnedDescendants_(); var all = od.all; int iLastTopmost = -1; for (int i = 0; i < all.Length && all[i].IsTopmost; i++) iLastTopmost = i; int iThis = Array.IndexOf(all, this); if (iThis < 0) return false; int iW = -1; if (top) { //make this the top window in the topmost or nontopmost group if (iThis > iLastTopmost && iLastTopmost >= 0) w = all[iW = iLastTopmost]; } else if (!w.Is0) { iW = Array.IndexOf(all, w); if (iW < 0) return false; } bool wasTopmost = iThis <= iLastTopmost; bool willBeTopmost = iW < iLastTopmost || (iW == iLastTopmost && wasTopmost); //descendants bool skipTopmostOwned = !willBeTopmost && !wasTopmost; List ai = od.GetIndices(this, skipTopmostOwned ? i => i <= iLastTopmost || i == iW : i => i == iW); ai.Add(iThis); //owners int iSkip = iThis; for (wnd ow = Get.Owner; !ow.Is0; ow = ow.Get.Owner) { int i2 = Array.IndexOf(all, ow); if (i2 < 0 || i2 == iW) break; if (willBeTopmost && i2 > iLastTopmost) break; //ignore nontopmost owners if (!ownerToo && i2 > iW) break; //don't zorder owners unless they would be on top of this List ai2 = od.GetIndices(ow, i => i == iSkip || (!willBeTopmost && i <= iLastTopmost)); ai.AddRange(ai2); ai.Add(iSkip = i2); } if (ai.Count == 1) return SetWindowPos(_SWP_ZORDER_NOOWNER, zorderAfter: w); //With DeferWindowPos faster and no flickering. // But it's unreliable. Eg can't change topmost/notopmost. // If fails, retry with SetWindowPos. bool dwpWillFail = willBeTopmost; //will fail if need to move above topmost uiAccess etc windows if (!dwpWillFail) { //will fail if need to change topmost/notopmost for (int i = 0; i < ai.Count; i++) { if (dwpWillFail = ai[i] <= iLastTopmost) break; } } var wPrev = w; if (!dwpWillFail) { var hp = Api.BeginDeferWindowPos(ai.Count); for (int i = 0; i < ai.Count; i++) { var k = all[ai[i]]; hp = Api.DeferWindowPos(hp, k, wPrev, 0, 0, 0, 0, _SWP_ZORDER_NOOWNER); if (hp == default) break; wPrev = k; } if (hp != default) { Api.EndDeferWindowPos(hp); //usually returns true when fails wPrev = w; for (int i = 0; ;) { var k = all[ai[i]]; if (k.Get.Previous() != wPrev) break; if (++i == ai.Count) return true; wPrev = k; } } Debug_.Print("DeferWindowPos failed"); wPrev = w; } for (int i = 0; i < ai.Count; i++) { var k = all[ai[i]]; bool ok = k.SetWindowPos(_SWP_ZORDER_NOOWNER, zorderAfter: wPrev); if (!ok) { if (k == this) return false; continue; } wPrev = k; } return true; } /// /// Places this window or control at the top of the Z order. Does not activate. /// If the window was topmost, it will be at the top of topmost windows, else at the top of other windows. /// /// Place owner windows below this window. If false (default), does it only if they otherwise would be on top of this window. /// /// Also affects owner and owned windows. /// Uses API SetWindowPos. /// Supports . /// public bool ZorderTop(bool ownerToo = false) => _ZorderAB(default, false, ownerToo, top: true); /// /// Calls SetWindowPos with HWND_TOP and SWP_NOOWNERZORDER (ignores owner and owned windows). /// internal bool ZorderTopRaw_() { Debug_.PrintIf(!IsChild && !Get.EnabledOwned().Is0, "has owned windows: " + ToString()); if (IsChild || IsTopmost || IsActive) return SetWindowPos(_SWP_ZORDER_NOOWNER); getwnd.top2(out var lastTopmost); return SetWindowPos(_SWP_ZORDER_NOOWNER, zorderAfter: lastTopmost); } /// /// Places this window or control at the bottom of the Z order. /// /// /// Also affects owner and owned windows. /// If the window was topmost, makes it and its owner and owned windows non-topmost. /// Uses API SetWindowPos. /// Supports . /// public bool ZorderBottom() { return SetWindowPos(_SWP_ZORDER_OWNER, zorderAfter: SpecHWND.BOTTOM); //Bug in older Windows 10: the first HWND_BOTTOM moves a topmost window to the top of non-topmost windows. } /// /// Makes this window topmost (always on top of non-topmost windows in the Z order). Does not activate. /// /// /// This cannot be a control. /// Also affects owned windows. Does not affect owners. /// Uses API SetWindowPos. /// Supports . /// public bool ZorderTopmost() { if (!SetWindowPos(_SWP_ZORDER_NOOWNER, zorderAfter: SpecHWND.TOPMOST)) return false; var a = Get.AllOwned(allDescendants: true); for (int i = a.Length; --i >= 0;) { a[i].SetWindowPos(_SWP_ZORDER_NOOWNER, zorderAfter: SpecHWND.TOPMOST); } return true; //note: cannot use without SWP_NOOWNERZORDER because of API bugs. } /// [Obsolete, EditorBrowsable(EditorBrowsableState.Never)] public bool ZorderTopmost(bool ownerToo) => (ownerToo ? Get.RootOwnerOrThis() : this).ZorderTopmost(); /// /// Makes this window non-topmost. /// /// Also place this window below the active non-topmost window in the Z order, unless the active window is this or owner. /// /// This cannot be a control. /// Also affects owner and owned windows. /// Uses API SetWindowPos. /// Supports . /// public bool ZorderNoTopmost(bool afterActiveWindow = false) { if (!IsTopmost) return IsAlive; if (afterActiveWindow) { wnd wa = active; if (wa != this && !wa.Is0 && !wa.IsTopmost && !Get.Owners().Contains(wa)) { if (SetWindowPos(_SWP_ZORDER_OWNER, zorderAfter: wa) && !IsTopmost) return true; } } return SetWindowPos(_SWP_ZORDER_OWNER, zorderAfter: SpecHWND.NOTOPMOST); } //Windows 7-10 bug: cannot make a topmost uiAccess window non-topmost (with HWND_NOTOPMOST, HWND_BOTTOM or non-topmost hwndInsertAfter). // Workaround: call SWP 2 times. With some Windows updates also need SWP_NOOWNERZORDER. // It seems it's fixed in current Win10. //Bug with SWP_NOOWNERZORDER: if used with non-uiAccess windows, then later HWND_TOPMOST does not work. // It seems it's fixed in current Win10. Can reproduce on Win8.1. //More problems with topmost uiAccess windows: // Can't insert a uiAccess window after a normal window. // Problems with HWND_BOTTOM and owned windows. // And so on. Possibly some now fixed. /// /// Returns true if this is a topmost (always-on-top) window. /// public bool IsTopmost => HasExStyle(WSE.TOPMOST); /// /// Returns true if this window is above window w in the Z order. /// public bool ZorderIsAbove(wnd w) { if (w.Is0) return false; for (wnd t = this; !t.Is0;) { t = Api.GetWindow(t, Api.GW_HWNDNEXT); if (t == w) return true; } return false; } #endregion #region style, exStyle /// /// Gets window style. /// /// One or more flags and/or class-specific style flags. Reference: window styles. /// Supports . /// /// public WS Style { get => (WS)(uint)GetWindowLong(GWL.STYLE); } /// /// Gets window extended style. /// /// One or more flags. Reference: extended window styles. /// Supports . /// /// public WSE ExStyle { get => (WSE)(uint)GetWindowLong(GWL.EXSTYLE); } /// /// Returns true if the window has all specified style flags (see ). /// /// One or more styles. /// /// Return true if has any (not necessary all) of the specified styles. /// Note: don't use , because it consists of two other styles - BORDER and DLGFRAME. /// /// Supports . public bool HasStyle(WS style, bool any = false) { var k = Style & style; return any ? k != 0 : k == style; } /// /// Returns true if the window has all specified extended style flags (see ). /// /// One or more extended styles. /// Return true if has any (not necessary all) of the specified styles. /// Supports . public bool HasExStyle(WSE exStyle, bool any = false) { var k = ExStyle & exStyle; return any ? k != 0 : k == exStyle; } /// /// Changes window style. /// /// One or more flags and/or class-specific style flags. Reference: window styles. /// /// Previous value. /// /// public WS SetStyle(WS style, WSFlags flags = 0) => (WS)_SetStyle(false, (int)style, flags); /// /// Changes window extended style. /// /// One or more flags. Reference: extended window styles. /// /// Previous value. /// /// public WSE SetExStyle(WSE style, WSFlags flags = 0) => (WSE)_SetStyle(true, (int)style, flags); nint _SetStyle(bool ex, int style, WSFlags flags) { var gwl = ex ? GWL.EXSTYLE : GWL.STYLE; switch (flags & (WSFlags.Add | WSFlags.Remove)) { case WSFlags.Add: style = (int)GetWindowLong(gwl) | style; break; case WSFlags.Remove: style = (int)GetWindowLong(gwl) & ~style; break; } nint R = SetWindowLong(gwl, style, noException: flags.Has(WSFlags.NoException)); if (flags.Has(WSFlags.UpdateNonclient)) SetWindowPos(SWPFlags.FRAMECHANGED | SWPFlags.NOMOVE | SWPFlags.NOSIZE | SWPFlags.NOZORDER | SWPFlags.NOOWNERZORDER | SWPFlags.NOACTIVATE); if (flags.Has(WSFlags.UpdateClient)) Api.InvalidateRect(this, true); return R; } /// /// Returns true if has style. /// /// Supports . public bool IsPopupWindow => HasStyle(WS.POPUP); /// /// Returns true if has style. /// /// Supports . public bool IsToolWindow => HasExStyle(WSE.TOOLWINDOW); /// /// Returns true if has style. /// /// Supports . public bool IsResizable => HasStyle(WS.THICKFRAME); #endregion #region window/class long, control id, prop /// /// Calls API GetWindowLongPtr. /// /// A constant from , or an offset in window memory reserved when registering window class. /// /// Supports . /// In 32-bit process actually calls GetWindowLong, because GetWindowLongPtr is unavailable. /// public nint GetWindowLong(int index) => Api.GetWindowLongPtr(this, index); /// /// Calls API SetWindowLongPtr. /// /// A constant from , or an offset in window memory reserved when registering window class. /// New value. /// Don't throw exception when fails. /// Previous value. If fails and noException true, returns 0, and can be used . /// /// /// See also API SetWindowSubclass. /// public nint SetWindowLong(int index, nint newValue, bool noException = false) { lastError.clear(); var prev = Api.SetWindowLongPtr(this, index, newValue); if (prev == 0 && !noException) { var e = lastError.code; if (e != 0 && (Api.GetWindowLongPtr(this, index) != newValue || !IsAlive)) ThrowUseNative(e); //if GWL_HWNDPARENT(0), last error "invalid window handle" } return prev; } /// /// Gets or sets id of this control. /// The get function supports . /// /// The set function failed. public int ControlId { get => Api.GetDlgCtrlID(this); set { SetWindowLong(GWL.ID, value); } } /// /// Returns an object that manages window properties using API SetProp and co. /// /// /// /// public WProp Prop => new(this); /// /// Returns an object that manages the taskbar button of this window: flash, progress, add/delete. /// /// /// /// public WTaskbarButton TaskbarButton => new(this); #endregion #region thread, process, is Unicode, is 64-bit, is hung/ghost, is console, UAC, is message-only /// /// Gets native thread id and process id of this window. /// Calls API GetWindowThreadProcessId. /// /// Thread id, or 0 if failed. Supports . /// /// It is not the same as . /// public unsafe int GetThreadProcessId(out int processId) { int pid = 0, tid = Api.GetWindowThreadProcessId(this, &pid); processId = pid; return tid; } /// /// Gets native thread id of this window. Calls API GetWindowThreadProcessId. /// /// 0 if failed. Supports . /// /// It is not the same as . /// public int ThreadId => Api.GetWindowThreadProcessId(this, null); //2 times faster when pid null /// /// Gets native process id of this window. Calls API GetWindowThreadProcessId. /// /// 0 if failed. Supports . public int ProcessId { get { GetThreadProcessId(out var pid); return pid; } } /// /// Returns true if this window belongs to the current thread, false if to another thread. /// Also returns false when fails (probably window closed or 0 handle). Supports . /// Calls API GetWindowThreadProcessId. /// public bool IsOfThisThread => Api.GetCurrentThreadId() == ThreadId; /// /// Returns true if this window belongs to the current process, false if to another process. /// Also returns false when fails (probably window closed or 0 handle). Supports . /// Calls API GetWindowThreadProcessId. /// public bool IsOfThisProcess => Api.GetCurrentProcessId() == ProcessId; /// /// Returns true if the window is a Unicode window, false if ANSI. /// Also returns false when fails (probably window closed or 0 handle). Supports . /// Calls API IsWindowUnicode. /// public bool IsUnicode => Api.IsWindowUnicode(this); /// /// Returns true if the window is of a 32-bit process, false if of a 64-bit process. /// Also returns false if failed. Supports . /// /// /// If you know that the window belongs to current process, instead use functions or IntPtr.Size==4. This function is much slower. /// public bool Is32Bit => process.is32Bit(ProcessId); /// /// Returns true if thread of this window is considered hung (not responding). /// Calls API IsHungAppWindow. /// /// Supports . public bool IsHung => Api.IsHungAppWindow(this); /// /// Returns true if the window is a ghost window that the system creates over a hung (not responding) window to allow the user to minimally interact with it. /// public bool IsHungGhost { get => IsHung && ClassNameIs("Ghost") && ProgramName.Eqi("DWM.exe"); //Class is "Ghost", exe is "DWM" (even if no Aero), text sometimes ends with "(Not Responding)". //IsHungWindow returns true for ghost window, although it is not actually hung. It is the fastest. //TEST: undocumented API HungWindowFromGhostWindow } /// /// Returns true if this is a console window (class name "ConsoleWindowClass"). /// /// Supports . public bool IsConsole => ClassNameIs("ConsoleWindowClass"); //FUTURE: GetConsoleText. See QM2. internal void UacCheckAndThrow_(string prefix = null) { if (!Is0 && UacAccessDenied) { if (prefix == null) prefix = "Failed. The"; else if (prefix.Ends('.')) prefix += " The"; //this is to support prefix used by mouse.move: "The active" throw new AuException(Api.ERROR_ACCESS_DENIED, prefix + " window's process has a higher UAC integrity level (admin or uiAccess) than this process."); } } /// /// Gets UAC info of the process. /// public uacInfo Uac => uacInfo.ofProcess(ProcessId); /// /// Returns true if [](xref:uac) would not allow to automate the window. /// It happens when current process has lower UAC integrity level and is not uiAccess, unless UAC is turned off. /// /// Supports . public bool UacAccessDenied { get { if (uacInfo.isAdmin) return false; lastError.clear(); //Api.RemoveProp(this, 0); //documented ERROR_ACCESS_DENIED, and used to work, but on latest Win10 GetLastError always returns 0. Api.SetWindowLongPtr(this, -1111, 0); //ERROR_INVALID_INDEX or ERROR_ACCESS_DENIED return lastError.code == Api.ERROR_ACCESS_DENIED; } } //These are not useful. Use UacAccessDenied or class uacInfo. ///// ///// Gets UAC integrity level of window's process. ///// Returns UacIL.Unknown if failed. ///// This function considers UIAccess equal to High. ///// See also: class . ///// //public UacIL UacIntegrityLevel //{ // get { var p = uacInfo.ofProcess(ProcessId); return p == null ? UacIL.Unknown : p.IntegrityLevel; } //} ///// ///// Returns true if window's process has higher UAC integrity level (IL) than current process. ///// Returns true if fails to open process handle, which usually means that the process has higher integrity level. ///// This function considers UIAccess equal to High. ///// See also: class . ///// //public bool UacIntegrityLevelIsHigher //{ // get => UacIntegrityLevel > uacInfo.ofThisProcess.IntegrityLevel; //} /// /// Returns true if this is a message-only window. /// /// /// To find message-only windows use . /// public bool IsMessageOnly { get { var t = HasStyle(WS.CHILD) ? Window : this; var v = t.ParentGWL_; if (v != default) { if (!s_messageOnlyParent.IsAlive) { for (int i = 0; i < 5; i++) { var w = findFast(null, null, true); //find a message-only window if (w.Is0) return false; //if there are no message-only windows, this window isn't message only w = w.ParentGWL_; if (!w.Is0) { s_messageOnlyParent = w; break; } //if 0, either w destroyed/reparented or GetWindowLong(GWL_HWNDPARENT) always returns 0 on this OS version } Debug.Assert(!s_messageOnlyParent.Is0); } return v == s_messageOnlyParent; } return false; } } static wnd s_messageOnlyParent; #endregion #region text, class, program /// /// Gets window class name. /// /// null if failed, eg if the window is closed. Supports . [SkipLocalsInit] public string ClassName { get { var b = stackalloc char[260]; int n = Api.GetClassName(this, b, 260); if (n == 0) return null; return new string(b, 0, n); } } /// /// Returns true if the class name of this window matches cn. Else returns false. /// Also returns false when fails (probably window closed or 0 handle). Supports . /// /// Class name. Case-insensitive wildcard. See . Cannot be null. /// public bool ClassNameIs(string cn) => ClassName.Like(cn, true); /// /// If window class name matches one of strings in classNames, returns 1-based string index. Else returns 0. /// Also returns 0 if fails to get class name (probably window closed or 0 handle). Supports . /// /// Class names. Case-insensitive wildcard. See . The array and strings cannot be null. public int ClassNameIs(params ReadOnlySpan classNames) { string s = ClassName; if (s == null) return 0; return s.Like(true, classNames); } /// /// Gets name. /// /// Returns "" if no name. Returns null if failed, eg if the window is closed. Supports . /// /// Top-level window name usually its title bar text. /// Control name usually is its text that does not change, for example button or static (label) control text. /// Unlike , this function usually does not get variable text, for example Edit control editable text, combo box control selected item text, status bar text. /// Calls (false, true). /// /// /// /// /// public string Name => GetText(false, true); /// /// If window name matches one of strings in names, returns 1-based string index. Else returns 0. /// Also returns 0 if fails to get name (probably window closed or 0 handle). Supports . /// /// Window names. Case-insensitive wildcard. See . The array and strings cannot be null. public int NameIs(params ReadOnlySpan names) { string s = Name; if (s == null) return 0; return s.Like(true, names); } /// /// Gets window name using API InternalGetWindowText. The same as GetText(false, false). /// This should be a top-level window, because does not process ampersands. /// internal string NameTL_ => _GetTextFast(false); /// /// Gets control text. /// /// Returns "" if no text. Returns null if failed, eg if the window is closed. Supports . /// /// Unlike , this function prefers variable text, for example Edit control text, combo box selected item text, status bar text. /// For controls that cannot have such text (eg button, static), it usually gets the same text as Name. For example button and static (label) controls. /// Much slower than Name. Fails if the window is hung. /// Calls (true, false). /// /// /// public string ControlText => GetText(true, false); /// /// Gets window/control name or control text. /// This is a low-level function. You can instead use and . /// /// Returns "" if no text. Returns null if failed, eg if the window is closed. Supports . /// /// How to get text: ///
false - use API InternalGetWindowText. This is used by . ///
true - use API WM_GETTEXT. It is slow and prefers editable text. This is used by . Fails if the window is hung. ///
null - try InternalGetWindowText. If it gets "" and this is a control, then try WM_GETTEXT. /// /// /// Remove the invisible '&' characters that are used to underline keyboard shortcuts with the Alt key. /// Removes only if this is a control (has style ). /// Calls . /// /// /// /// public string GetText(bool? getText = null, bool removeUnderlineAmpersand = true) { var R = getText switch { true => _GetTextSlow(), false => _GetTextFast(false), _ => _GetTextFast(true) }; if (removeUnderlineAmpersand && !R.NE() //&& R.Contains('&') //slower than HasStyle if the string is longer than 20 && HasStyle(WS.CHILD) ) R = StringUtil.RemoveUnderlineChar(R); return R; } /// /// Gets text. /// Returns "" if it is empty. /// Calls API InternalGetWindowText. If it fails, and getControlTextIfEmpty==true, and this is a control, calls _GetTextSlow, which uses WM_GETTEXT. /// /// null if failed, eg if the control is destroyed or its thread is hung. Supports . [SkipLocalsInit] string _GetTextFast(bool useSlowIfEmpty) { if (Is0) { lastError.code = Api.ERROR_INVALID_WINDOW_HANDLE; return null; } //return early. Used in properties of wnd and other types. They can be called not explicitly from code, eg by debugger, record.ToString(), etc. Eg a record may have a property of wnd type, and it may be 0 (it's valid). using FastBuffer b = new(); for (; ; b.More()) { lastError.clear(); int nr = Api.InternalGetWindowText(this, b.p, b.n); if (nr < b.n - 1) { if (nr > 0) return new string(b.p, 0, nr); if (lastError.code != 0) return null; if (useSlowIfEmpty && HasStyle(WS.CHILD)) return _GetTextSlow(); return ""; } } } /// /// Gets text. /// Returns "" if it is empty. /// Uses WM_GETTEXT. /// /// null if failed, eg if the control is destroyed or its thread is hung. Supports . [SkipLocalsInit] string _GetTextSlow() { if (Is0) { lastError.code = Api.ERROR_INVALID_WINDOW_HANDLE; return null; } if (!SendTimeout(5000, out nint n, Api.WM_GETTEXTLENGTH)) return null; if (n < 1) return ""; using FastBuffer b = new((int)n + 1); if (!SendTimeout(30000, out n, Api.WM_GETTEXT, b.n, b.p)) return null; if (n < 1) return ""; b.p[n] = '\0'; return b.GetStringFindLength(); //info: some controls return incorrect n, eg including '\0' //note: cannot do this optimization: // At first allocate stack memory and send WM_GETTEXT without WM_GETTEXTLENGTH. Then use WM_GETTEXTLENGTH/WM_GETTEXT if returned size is buffer length - 1. // It works with most controls, but some controls return 0 if buffer is too small. Eg SysLink. The WM_GETTEXT documentation does not say what should happen when buffer is too small. // The speed is important for wnd.Child(). // It is ~30% faster when all controls have text, but not much if called for many controls that don't have text (then we don't use WM_GETTEXT). } /// /// Sets window/control name or control text. /// /// Text. Can be null, it is the same as "". /// /// Uses API WM_SETTEXT. /// Top-level window name usually its title bar text. /// For variable-text controls (edit, combo box, status bar, ...) this usually is the text that would get. /// For other controls (button, static, ...) and top-level windows this usually is the text that would get. /// /// Failed, for example the window is closed. /// /// /// public void SetText(string text) { if (!SendTimeout(30000, out var _, Api.WM_SETTEXT, 0, text ?? "", 0)) ThrowUseNative(); } //rejected: faster but not better than NameElm. Or must be improved. // Instead, let Child find label control, and then navigate with Get.Right() etc. ///// ///// Gets of previous (in Z order) sibling control. ///// Returns null if there is no such control. ///// ///// ///// Can be used to identify controls that have no name but have a named control (label) at the left or above. For example Edit and ComboBox controls in dialogs. ///// In such cases this function works like , but is faster and supports any sibling control type. ///// //public string NameLabel //{ // get // { // for(var w = this; ;) { // var p = w.Get.DirectParent; // if(p.Is0) return null; // w = w.Get.Previous(); if(!w.Is0) return w.Name; // w = p; // } // //_todo: if the label control is not at the left/above, find topmost control at the left/above // } //} /// /// Gets of the UI element (role WINDOW) of this window or control. /// /// Returns "" if the object has no name or failed to get it. Returns null if invalid window handle. public string NameElm => elm.NameOfWindow_(this); /// /// Gets the property value of a .NET Windows Forms control. /// /// null if it is not a Windows Forms control or if failed. /// /// Use this with controls of other processes. Don't use with your controls, when you have a Control object. /// /// Slow when getting names of multiple controls in a window. Instead create a instance and call its method for each control. /// /// public string NameWinforms => WinformsControlNames.GetSingleControlName(this); /// /// Gets filename of process executable file, like "notepad.exe". /// /// null if failed. /// /// Calls and . /// This function is much slower than getting window name or class name. Don't use code like if(w.ProgramName=="A" || w.ProgramName=="B"). Instead use var s=w.ProgramName; if(s=="A" || s=="B"). /// public string ProgramName => process.GetNameCached_(this, ProcessId); /// /// If window program name matches one of strings in programNames, returns 1-based string index. Else returns 0. /// Also returns 0 if fails to get program name (probably window closed or 0 handle). Supports . /// /// Program names, like "notepad.exe". Case-insensitive wildcard. See . The array and strings cannot be null. public int ProgramNameIs(params ReadOnlySpan programNames) { string s = ProgramName; if (s == null) return 0; return s.Like(true, programNames); } /// /// Gets full path of process executable file. /// /// null if failed. /// /// Calls and . /// This function is much slower than getting window name or class name. Don't use code like if(w.ProgramPath=="A" || w.ProgramPath=="B"). Instead use var s=w.ProgramPath; if(s=="A" || s=="B"). /// public string ProgramPath => process.GetNameCached_(this, ProcessId, true); /// /// Gets description of process executable file. /// /// null if failed. /// /// Calls and . /// This function is slow. Much slower than . /// public string ProgramDescription => process.getDescription(ProcessId); #endregion #region close, destroy /// /// Closes the window. /// /// true if successfully closed or if it was already closed (the handle is 0 or invalid) or if noWait==true. /// /// If true, does not wait until the window is closed. /// If false, waits about 1 s (depends on window type etc) until the window is destroyed or disabled. /// /// /// If false (default), uses API message WM_CLOSE. /// If true, uses API message WM_SYSCOMMAND SC_CLOSE, like when the user clicks the X button in the title bar. /// Most windows can be closed with any of these messages, but some respond properly only to one of them. For example, some applications on WM_CLOSE don't exit, although the main window is closed. Some applications don't respond to WM_SYSCOMMAND if it is posted soon after opening the window. /// /// /// The window may refuse to be closed. For example, it may be hung, or hide itself instead, or display a "Save?" message box, or is a dialog without X button, or just need more time to close it. /// If the window is of this thread, just calls or (if noWait==true) and returns true. /// /// /// /// /// public bool Close(bool noWait = false, bool useXButton = false) { if (!IsAlive) return true; int msg = Api.WM_CLOSE; int wparam = 0; if (useXButton) { msg = Api.WM_SYSCOMMAND; wparam = Api.SC_CLOSE; } if (IsOfThisThread) { if (noWait) Post(msg, wparam); else Send(msg, wparam); return true; } //Some windows cannot be properly closed using only WM_CLOSE. For example, VS 7 window closed, but the process not. //Some windows (eg IE), ignore SC_CLOSE if it is posted soon after starting the program. Only if using Post, not if SendNotify. //When the user closes a window, Windows sends SC_CLOSE. DefWindowProc on SC_CLOSE sends WM_CLOSE, except if disabled. //Other apps use either WM_CLOSE or SC_CLOSE. // Task Manager and taskbar send (not post) SC_CLOSE. // Process Explorer and taskkill post WM_CLOSE. //For our purposes WM_CLOSE is probably better. // QM2 used SC_CLOSE or WM_CLOSE depending on window style. //note: Don't use SendX messages because of possible crashes and other anomalies. // Eg it can hide (not close) some windows, and next app instance does not start (eg Firefox). // Imagine if the target window's app is in SendMessage which dispatches foreign messages, and after it returns, the window is already died. bool ok = Post(msg, wparam); if (!ok) { //print.it(lastError.code); //0 when UAC access denied if (!useXButton) ok = Post(Api.WM_SYSCOMMAND, Api.SC_CLOSE); //UAC blocks WM_CLOSE but not WM_SYSCOMMAND } //if(!noWait.HasValue) noWait = this_thread_is_UI; //rejected if (noWait) return true; if (ok) { for (int i = 0; i < 100; i++) { Thread.Sleep(15); if (!IsEnabled(false)) break; //destroyed or has an owned modal dialog box, eg "Save?" //Wait less if hidden, eg has a tray icon. //Also if a popup, eg a Yes/No message box (disabled X button). //Also if child. //if(!IsVisible && !_IsBusy(2)) i += 4; //unreliable, too dirty if (!IsVisible || (Style & (WS.POPUP | WS.CHILD)) != 0) i += 2; if (i >= 50) { if (!SendTimeout(200, out _, 0)) { if (!IsAlive || IsHung) break; } } } } MinimalSleepNoCheckThread_(); WndUtil.WaitForAnActiveWindow(); return !IsAlive; } //bool _IsBusy(int milliseconds) //{ // //Need to measure time. Cannot use just 2 ms timeout and ST return value because of the system timer default period 15.6 ms etc. // var t = perf.mcs; // SendTimeout(5 + milliseconds, 0, flags: 0); // var d = perf.mcs - t; // //print.it(d); // return (d >= milliseconds * 1000L); //} //Rarely used. It is easy, and there is example in Close() help: foreach (var w in wnd.findAll("* Notepad", "Notepad")) w.Close(); ///// ///// Closes all matching windows. ///// Calls . All parameters etc are the same. Then calls for each found window. ///// Returns the number of found windows. ///// //public static int closeAll( // string name, string cn = null, WOwner of = default, // WFlags flags = 0, Func f = null, WContains contains = default // ) //{ // var a = FindAll(name, cn, of, flags, f, contains); // foreach(wnd w in a) w.Close(); // return a.Count; //} #endregion } ================================================ FILE: Au/wnd/wndChildFinder.cs ================================================ using static Au.wnd.Internal_; namespace Au; /// /// Finds window controls (child windows). Contains name and other parameters of controls to find. /// /// /// Can be used instead of or . /// Also can be used to find window that contains certain control, like in examples. /// /// /// Find window that contains certain control, and get the control too. /// t.HasChild(cf)); /// print.it(w); /// print.it(cf.Result); /// ]]> /// The same with parameter contains. /// /// public class wndChildFinder { enum _NameIs : byte { name, text, elmName, wfName } readonly wildex _name; readonly wildex _cn; readonly Func _also; WinformsControlNames _wfControls; readonly int _skipCount; readonly WCFlags _flags; readonly int? _id; readonly _NameIs _nameIs; /// /// See . /// /// /// - name starts with "***", but the prefix is invalid. /// - cn is "". To match any, use null. /// - Invalid wildcard expression ("**options " or regular expression). /// /// public wndChildFinder( [ParamString(PSFormat.Wildex)] string name = null, [ParamString(PSFormat.Wildex)] string cn = null, WCFlags flags = 0, int? id = null, Func also = null, int skip = 0 ) { if (cn != null) { if (cn.Length == 0) throw new ArgumentException("Class name cannot be \"\". Use null."); _cn = cn; } if (name != null) { switch (StringUtil.ParseParam3Stars_(ref name, "text", "elmName", "wfName"/*, "label"*/)) { case -1: throw new ArgumentException("Invalid name prefix. Can be: \"***text \", \"***elmName \", \"***wfName \"."); //, \"***label \" case 1: _nameIs = _NameIs.text; break; case 2: _nameIs = _NameIs.elmName; break; case 3: _nameIs = _NameIs.wfName; break; //case 4: _nameIs = _NameIs.label; break; } _name = name; } _flags = flags; _id = id; _also = also; _skipCount = skip; } /// /// The found control. /// public wnd Result { get; internal set; } /// /// Finds the specified child control, like . /// /// If found, returns , else default(wnd). /// Direct or indirect parent window. Can be top-level window or control. /// Invalid wParent. /// /// Functions Find and differ only in their return types. /// public wnd Find(wnd wParent) => Exists(wParent) ? Result : default; /// /// Finds the specified child control, like . Can wait and throw . /// /// If found, returns . Else throws exception or returns default(wnd) (if wait negative). /// Direct or indirect parent window. Can be top-level window or control. /// The wait timeout, seconds. If 0, does not wait. If negative, does not throw exception when not found. /// Invalid wParent. /// /// /// Functions Find and differ only in their return types. /// public wnd Find(wnd wParent, Seconds wait) => Exists(wParent, wait) ? Result : default; /// If found, sets and returns true, else false. /// public bool Exists(wnd wParent) { using var k = new WndList_(_AllChildren(wParent)); return _FindInList(wParent, k) >= 0; } /// If found, sets and returns true. Else throws exception or returns false (if wait negative). /// public bool Exists(wnd wParent, Seconds wait) { var r = wait.Exists_() ? Exists(wParent) : Au.wait.until(wait, () => Exists(wParent)); return r || wait.ReturnFalseOrThrowNotFound_(); } ArrayBuilder_ _AllChildren(wnd wParent) { wParent.ThrowIfInvalid(); return EnumWindows2(EnumAPI.EnumChildWindows, onlyVisible: 0 == (_flags & WCFlags.HiddenToo), sortFirstVisible: true, wParent: wParent, directChild: 0 != (_flags & WCFlags.DirectChild)); } /// /// Finds the specified control in a list of controls. /// The property will be the control. /// /// 0-based index, or -1 if not found. /// List of controls, for example returned by . /// Direct or indirect parent window. Used only for flag DirectChild. public int FindInList(IEnumerable a, wnd wParent = default) { using var k = new WndList_(a); return _FindInList(wParent, k); } /// /// Finds all matching child controls, like . /// /// Array containing zero or more . /// Direct or indirect parent window. Can be top-level window or control. /// Invalid wParent. public wnd[] FindAll(wnd wParent) { return _FindAll(new WndList_(_AllChildren(wParent)), wParent); } /// /// Finds all matching controls in a list of controls. /// /// Array containing zero or more . /// List of controls, for example returned by . /// Direct or indirect parent window. Used only for flag DirectChild. public wnd[] FindAllInList(IEnumerable a, wnd wParent = default) { return _FindAll(new WndList_(a), wParent); } wnd[] _FindAll(WndList_ k, wnd wParent) { using (k) { using var ab = new ArrayBuilder_(); _FindInList(wParent, k, w => ab.Add(w)); //CONSIDER: ab could be part of _WndList. Now the delegate creates garbage. return ab.ToArray(); } } /// /// Returns index of matching element or -1. /// /// Parent window. Can be default(wnd) if inList and no DirectChild flag and not using winforms name. /// List of . Does not dispose it. /// If not null, calls it for all matching and returns -1. int _FindInList(wnd wParent, WndList_ a, Action getAll = null) { Result = default; if (a.Type == WndList_.ListType.None) return -1; bool inList = a.Type != WndList_.ListType.ArrayBuilder; int skipCount = _skipCount; try { //will need to dispose something for (int index = 0; a.Next(out wnd w); index++) { if (w.Is0) continue; if (inList) { //else the enum function did this if (!_flags.Has(WCFlags.HiddenToo)) { if (!w.IsVisibleIn_(wParent)) continue; } if (_flags.Has(WCFlags.DirectChild) && !wParent.Is0) { if (w.ParentGWL_ != wParent) continue; } } if (_id != null) { if (w.ControlId != _id.Value) continue; } if (_cn != null) { if (!_cn.Match(w.ClassName)) continue; } if (_name != null) { string s; switch (_nameIs) { case _NameIs.text: s = w.ControlText; break; case _NameIs.elmName: s = w.NameElm; break; case _NameIs.wfName: if (_wfControls == null) { try { _wfControls = new WinformsControlNames(wParent.Is0 ? w : wParent); } catch (AuWndException) { //invalid parent window return -1; } catch (AuException e) { //probably process of higher UAC integrity level print.warning($"Failed to get winforms control names. {e.Message}"); return -1; } } s = _wfControls.GetControlName(w); break; //case _NameIs.label: // s = w.NameLabel; // break; default: s = w.Name; break; } if (!_name.Match(s)) continue; } if (_also != null && !_also(w)) continue; if (getAll != null) { getAll(w); continue; } if (skipCount-- > 0) continue; Result = w; return index; } } finally { if (_wfControls != null) { _wfControls.Dispose(); _wfControls = null; } } return -1; } /// /// Returns true if control c properties match the specified properties. /// /// A control. Can be 0/invalid, then returns false. /// Direct or indirect parent window. If used, returns false if it isn't parent (also depends on flag DirectChild). public bool IsMatch(wnd c, wnd wParent = default) { if (!wParent.Is0 && !c.IsChildOf(wParent)) { Result = default; return false; } return 0 == _FindInList(wParent, new WndList_(c)); } /// public override string ToString() { using (new StringBuilder_(out var b)) { b.Append('['); _Append("name", _name, true); _Append("cn", _cn, true); if (_id != null) _Append("id", _id.ToString(), false); if (_flags != 0) _Append("flags", _flags.ToString(), false); if (_also != null) _Append("also", "...", false); if (_skipCount != 0) _Append("skip", _skipCount.ToString(), false); b.Append(']'); return b.ToString(); void _Append(string k, object v, bool isString) { if (v == null) return; if (b.Length > 1) b.Append(", "); var s = v.ToString(); if (isString) s = s.Escape(limit: 50, quote: true); b.Append(k).Append(": ").Append(s); } } } } ================================================ FILE: Au/wnd/wndFinder.cs ================================================ using static Au.wnd.Internal_; //TODO3: if no name, prefer non-tooltip window. Now often finds tooltip instead of the wanted window. Eg Java menus. namespace Au; /// /// Finds top-level windows (). Contains name and other parameters of windows to find. /// /// /// Can be used instead of or . /// These codes are equivalent: /// /// /// Also can find in a list of windows. /// public class wndFinder { readonly wildex _name; readonly wildex _cn; readonly wildex _program; readonly int _processId; readonly int _threadId; readonly wnd _owner; readonly Func _also; readonly WFlags _flags; readonly WContains _contains; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member /// /// Parsed parameter values. All read-only. /// public TProps Props => new(this); [NoDoc] public struct TProps { readonly wndFinder _f; internal TProps(wndFinder f) { _f = f; } public wildex name => _f._name; public wildex cn => _f._cn; public wildex program => _f._program; public int processId => _f._processId; public int threadId => _f._threadId; public wnd owner => _f._owner; public WFlags flags => _f._flags; public Func also => _f._also; public WContains contains => _f._contains; /// /// After unsuccessful indicates the parameter that does not match. /// public EProps DoesNotMatch => _f._stopProp; } EProps _stopProp; [NoDoc] public enum EProps { name = 1, cn, of, also, contains, visible, cloaked, } public override string ToString() { using (new StringBuilder_(out var b)) { _Append("name", _name); _Append("cn", _cn); if (_program != null) _Append("program", _program); else if (_processId != 0) _Append("processId", _processId); else if (_threadId != 0) _Append("threadId", _threadId); if (_also != null) _Append("also", ""); _Append("contains", _contains.Value); return b.ToString(); void _Append(string k, object v) { if (v == null) return; if (b.Length != 0) b.Append(", "); b.Append(k).Append('=').Append(v); } } } #pragma warning restore CS1591 /// public wndFinder( [ParamString(PSFormat.Wildex)] string name = null, [ParamString(PSFormat.Wildex)] string cn = null, [ParamString(PSFormat.Wildex)] WOwner of = default, WFlags flags = 0, Func also = null, WContains contains = default) { _name = name; if (cn != null) _cn = cn.Length != 0 ? cn : throw new ArgumentException("cn cannot be \"\". Use null."); of.GetValue(out _program, out _processId, out _threadId, out _owner); _flags = flags; _also = also; _contains = contains; } //rejected. Better slightly longer code than unclear and possibly ambiguous code where need to learn string parsing rules. ///// ///// Implicit conversion from string that can contain window name, class name, program and/or a contains object. ///// Examples: "name,cn,program", "name", ",cn", "*,,program", "name,cn", "name,,program", ",cn,program", "name,,,object". ///// ///// ///// One or more comma-separated window properties: name, class, program and/or a contains object. Empty name means "" (for any use *); other empty parts mean null. ///// The same as parameters of . The first 3 parts are name, cn and of. The last part is contains as string; can specify a UI element, control or image. ///// The first 3 comma-separated parts cannot contain commas. Alternatively, parts can be separated by '\0' characters, like "name\0"+"cn\0"+"program\0"+"object". Then parts can contain commas. Example: "*one, two, three*\0" (name with commas). ///// ///// See . ///// If specifies a contains object: exceptions of constructor of or or . //public static implicit operator wndFinder(string s) { // string name = null, cn = null, prog = null, contains = null; // char[] sep = null; if (s.Contains('\0')) sep = s_sepZero; else if (s.Contains(',')) sep = s_sepComma; // if (sep == null) name = s; // else { // var ra = s.Split(sep, 4); // name = ra[0]; // if (ra[1].Length > 0) cn = ra[1]; // if (ra.Length > 2 && ra[2].Length > 0) prog = ra[2]; // if (ra.Length > 3 && ra[3].Length > 0) contains = ra[3]; // } // return new wndFinder(name, cn, prog, contains: contains); //} //static readonly char[] s_sepComma = { ',' }, s_sepZero = { '\0' }; /// /// The found window. /// public wnd Result { get; internal set; } /// /// Finds the specified window, like . /// /// If found, returns , else default(wnd). /// /// Functions Find and differ only in their return types. /// public wnd Find() => Exists() ? Result : default; /// /// Finds the specified window, like . Can wait and throw . /// /// If found, returns . Else throws exception or returns default(wnd) (if wait negative). /// The wait timeout, seconds. If 0, does not wait. If negative, does not throw exception when not found. /// /// /// Functions Find and differ only in their return types. /// public wnd Find(Seconds wait) => Exists(wait) ? Result : default; /// If found, sets and returns true, else false. /// public bool Exists() { using var k = new WndList_(_AllWindows()); return _FindOrMatch(k) >= 0; } /// If found, sets and returns true. Else throws exception or returns false (if wait negative). /// public bool Exists(Seconds wait) { var r = wait.Exists_() ? Exists() : !Wait(wait, false).Is0; return r || wait.ReturnFalseOrThrowNotFound_(); } /// /// Waits until window exists or is active. /// /// Returns . On timeout returns default(wnd) if timeout is negative; else exception. /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// The window must be the active window (), and not minimized. /// timeout time has expired (if > 0). /// /// Same as , except: /// - 0 timeout means infinite. /// - on timeout throws , not . /// - has parameter active. /// public wnd Wait(Seconds timeout, bool active) { var loop = new WaitLoop(timeout); for (; ; ) { if (active) { wnd w = wnd.active; if (IsMatch(w) && !w.IsMinimized) return Result = w; //CONSIDER: also wait until released mouse buttons. } else { if (Exists()) return Result; } if (!loop.Sleep()) return default; } } ArrayBuilder_ _AllWindows() { //FUTURE: optimization: if cn not wildcard etc, at first find atom. // If not found, don't search. If found, compare atom, not class name string. var f = _threadId != 0 ? EnumAPI.EnumThreadWindows : EnumAPI.EnumWindows; return EnumWindows2(f, 0 == (_flags & WFlags.HiddenToo), true, wParent: _owner, threadId: _threadId); } /// /// Finds the specified window in a list of windows, and sets . /// /// Returns 0-based index, or -1 if not found. /// Array or list of windows, for example returned by . public int FindInList(IEnumerable a) { using var k = new WndList_(a); return _FindOrMatch(k); } /// /// Finds all matching windows, like . /// /// Array containing 0 or more window handles as . public wnd[] FindAll() { return _FindAll(new WndList_(_AllWindows())); } /// /// Finds all matching windows in a list of windows. /// /// Array containing 0 or more window handles as . /// Array or list of windows, for example returned by . public wnd[] FindAllInList(IEnumerable a) { return _FindAll(new WndList_(a)); } wnd[] _FindAll(WndList_ k) { using (k) { using var ab = new ArrayBuilder_(); _FindOrMatch(k, w => ab.Add(w)); //CONSIDER: ab could be part of WndList_. Now the delegate creates garbage. return ab.ToArray(); } } /// /// Returns index of matching element or -1. /// Returns -1 if using getAll. /// /// List of . Does not dispose it. /// If not null, calls it for all matching and returns -1. /// int _FindOrMatch(WndList_ a, Action getAll = null, WFCache cache = null) { Result = default; _stopProp = 0; if (a.Type == WndList_.ListType.None) return -1; bool inList = a.Type != WndList_.ListType.ArrayBuilder; bool ignoreVisibility = cache?.IgnoreVisibility ?? false; bool mustBeVisible = inList && (_flags & WFlags.HiddenToo) == 0 && !ignoreVisibility; bool isOwner = inList && !_owner.Is0; int ownerTid = 0; bool isTid = inList ? _threadId != 0 : false; List pids = null; bool programNamePlanB = false; //variables for faster getting/matching program name int index = 0; for (; a.Next(out wnd w); index++) { if (w.Is0) continue; //Speed of getting properties of 1000 windows with hot CPU: //name 1000, class 1000 //foreign tid/pid 900/1800, //visible 30, cloaked 1100, //owner 40, style 40, exstyle 40, //isOwned(2) 2000, isOwned(2, ref tid) 1000, //rect 500, //GetProp(randomString) 1700, GetProp(existingString) 3000, GetProp(atom) 1000, GlobalFindAtom 1700, //program 6000 if (mustBeVisible) { if (!w.IsVisible) { _stopProp = EProps.visible; continue; } } if (isOwner) { if (!w.IsOwnedBy2_(_owner, 2, ref ownerTid)) { _stopProp = EProps.of; continue; } } cache?.Begin(w); if (_name != null) { var s = cache != null && cache.CacheName ? (cache.Name ??= w.NameTL_) : w.NameTL_; if (!_name.Match(s)) { _stopProp = EProps.name; continue; } //note: name is before classname. It makes faster in slowest cases (HiddenToo), because most windows are nameless. } if (_cn != null) { var s = cache != null ? (cache.Class ?? (cache.Class = w.ClassName)) : w.ClassName; if (!_cn.Match(s)) { _stopProp = EProps.cn; continue; } } if (0 == (_flags & WFlags.CloakedToo) && !ignoreVisibility) { if (w.IsCloaked) { _stopProp = EProps.cloaked; continue; } } int pid = 0, tid = 0; if (_program != null || _processId != 0 || isTid) { if (cache != null) { if (cache.Tid == 0) cache.Tid = w.GetThreadProcessId(out cache.Pid); tid = cache.Tid; pid = cache.Pid; } else tid = w.GetThreadProcessId(out pid); if (tid == 0) { _stopProp = EProps.of; continue; } //speed: with foreign processes the same speed as getting name or class name. Much faster if same process. } if (isTid) { if (_threadId != tid) { _stopProp = EProps.of; continue; } } if (_processId != 0) { if (_processId != pid) { _stopProp = EProps.of; continue; } } if (_program != null) { //Getting program name is one of slowest parts. //Usually it does not slow down much because need to do it only 1 or several times, only when window name, class etc match. //The worst case is when only program is specified, and the very worst case is when also using flag HiddenToo. //We are prepared for the worst case. //Normally we call process.getName. In most cases it is quite fast. //Anyway, we use this optimization: // Add pid of processes that don't match the specified name in the pids list (bad pids). // Next time, if pid is in the bad pids list, just continue, don't need to get program name again. //However in the worst case we would encounter some processes that process.getName cannot get name using the fast API. //For each such process it would then use the much slower 'get all processes' API, which is almost as slow as Process.GetProcessById(pid).ProgramName. //To solve this: //We tell process.getName to not use the slow API, but just return null when the fast API fails. //When it happens (process.getName returns null): // If need full path: continue, we cannot do anything more. // Switch to plan B and no longer use all the above. Plan B: // Get list of pids of all processes that match _program. For it we call process.GetProcessesByName_, which uses the same slow API, but we call it just one time. // If it returns null (it means there are no matching processes), break (window not found). // From now, in each loop will need just to find pid in the returned list, and continue if not found. _stopProp = EProps.of; g1: if (programNamePlanB) { if (!pids.Contains(pid)) continue; } else { if (pids != null && pids.Contains(pid)) continue; //is known bad pid? string pname = cache != null ? (cache.Program ?? (cache.Program = _Program())) : _Program(); string _Program() => process.getName(pid, false, true); //string _Program() => process.getName(pid, 0!=(_flags&WFlags.ProgramPath), true); if (pname == null) { //if(0!=(_flags&WFlags.ProgramPath)) continue; //switch to plan B process.GetProcessesByName_(ref pids, _program); if (pids.NE_()) break; programNamePlanB = true; goto g1; } if (!_program.Match(pname)) { if (a.Type == WndList_.ListType.SingleWnd) break; pids ??= new List(16); pids.Add(pid); //add bad pid continue; } } _stopProp = 0; } if (_also != null) { bool ok = false; try { ok = _also(w); } catch (AuWndException) { } //don't throw if w destroyed if (!ok) { _stopProp = EProps.also; continue; } } if (_contains.Value != null) { bool found = false; try { switch (_contains.Value) { case elmFinder f: found = w.HasElm(f); break; case wndChildFinder f: found = f.Exists(w); break; case uiimageFinder f: found = f.Exists(w); break; case ocrFinder f: found = f.Exists(w); break; } } catch (Exception ex) { if (!(ex is AuWndException)) print.warning("Exception when tried to find the 'contains' object. " + ex, -1); } if (!found) { _stopProp = EProps.contains; continue; } } if (getAll != null) { getAll(w); continue; } Result = w; return index; } if (index == 0 && !inList) if (!_owner.Is0 || _threadId != 0) _stopProp = EProps.of; return -1; } /// /// Returns true if window w properties match the specified properties. /// /// A top-level window. If 0 or invalid, returns false. /// Can be used to make faster when multiple variables are used with same window. The function gets window name/class/program once, and stores in cache; next time it gets these strings from cache. /// public bool IsMatch(wnd w, WFCache cache = null) { return 0 == _FindOrMatch(new WndList_(w), cache: cache); } } ================================================ FILE: Au/wnd/wnd_child.cs ================================================ namespace Au { public unsafe partial struct wnd { /// /// Finds a child control and returns its handle as . /// /// Returns default(wnd) if not found. See also: . /// /// Control name. /// String format: [wildcard expression](xref:wildcard_expression). /// null means "can be any". "" means "no name". /// /// By default to get control names this function uses . /// Can start with these prefix strings: ///
"***text " - use . Slower and less reliable because can get editable text. If a character can be underlined with Alt, insert '&' before it. ///
"***elmName " - use . Slower. ///
"***wfName " - use .NET Forms control name (see ). Slower and can fail because of [](xref:uac). /// /// /// Control class name. /// String format: [wildcard expression](xref:wildcard_expression). /// null means "can be any". Cannot be "". /// /// /// Control id. See . Not used if null (default). /// /// Callback function. Called for each matching control. /// It can evaluate more properties of the control and return true when they match. /// Example: also: t => t.IsEnabled /// /// /// 0-based index of matching control. /// For example, if 1, the function skips the first matching control and returns the second. /// /// This variable is invalid (window not found, closed, etc). /// /// - name starts with "***", but the prefix is invalid. /// - cn is "". To match any, use null. /// - Invalid wildcard expression ("**options " or regular expression). /// /// /// To create code for this function, use tool Find window. /// public wnd Child( [ParamString(PSFormat.Wildex)] string name = null, [ParamString(PSFormat.Wildex)] string cn = null, WCFlags flags = 0, int? id = null, Func also = null, int skip = 0 ) => new wndChildFinder(name, cn, flags, id, also, skip).Find(this); /// /// Finds a child control and returns its handle as . Can wait and throw . /// /// Child control handle. If not found, throws exception or returns default(wnd) (if wait negative). /// The wait timeout, seconds. If 0, does not wait. If negative, does not throw exception when not found. /// This variable is invalid (window not found, closed, etc). Or closed while waiting. /// /// public wnd Child( Seconds wait, [ParamString(PSFormat.Wildex)] string name = null, [ParamString(PSFormat.Wildex)] string cn = null, WCFlags flags = 0, int? id = null, Func also = null, int skip = 0 ) => new wndChildFinder(name, cn, flags, id, also, skip).Find(this, wait); /// /// Finds all matching child controls. /// /// Array containing zero or more . /// /// Everything except the return type is the same as with . /// /// In the returned array, hidden controls (when using ) are always after visible controls. /// /// /// public wnd[] ChildAll( [ParamString(PSFormat.Wildex)] string name = null, [ParamString(PSFormat.Wildex)] string cn = null, WCFlags flags = 0, int? id = null, Func also = null) { //ThrowIfInvalid(); //will be called later var f = new wndChildFinder(name, cn, flags, id, also); return f.FindAll(this); } /// /// Returns true if this window contains the specified control. /// Calls . /// /// /// /// Find window that contains certain control, and get the control too. /// t.HasChild(cf)); /// print.it(w); /// print.it(cf.Result); /// ]]> /// The same with parameter contains. /// /// public bool HasChild(wndChildFinder f) => f.Exists(this); //rejected. Rare. Can use w.HasChild(new("Apply", "Button")) or !w.Child(...).Is0 etc. Or would also need HasElm(string...). ///// ///// Returns true if this window contains the specified control. ///// Calls . ///// NOTE: Calling this function many times with same arguments is inefficient. Instead create new and call or . See example. ///// ///// ///// ///// //public bool HasChild( // [ParamString(PSFormat.Wildex)] string name = null, // [ParamString(PSFormat.Wildex)] string cn = null, // WCFlags flags = 0, int? id = null, Func also = null, int skip = 0) { // return default != Child(name, cn, flags, id, also, skip); //} /// /// Returns true if this window contains the specified UI element. /// Calls . /// /// /// /// Find window that contains certain UI element, and get the UI element too. /// t.HasElm(f)); /// print.it(w); /// print.it(f.Result); /// ]]> /// The same with parameter contains. /// /// public bool HasElm(elmFinder f) => f.Find_(false, this, null); //rejected. Use Child. Don't need 2 function for the same. Also the wait parameter is confusing. Also there is no finder. // This is faster and less garbage, but it's not so important. Also there is ChildFast(id) for direct children. ///// ///// Finds a child control by its id and returns its handle as . ///// ///// Child control handle, or default(wnd) if not found. See also: . ///// Control id. ///// This function supports flags DirectChild and HiddenToo. If both are set, it is much faster because uses API GetDlgItem. Else uses API EnumChildWindows, like . ///// This variable is invalid (window not found, closed, etc). ///// ///// To create code for this function, use tool Find window. ///// ///// Not all controls have a useful id. If control id is not unique or is different in each window instance, this function is not useful. ///// //public wnd ChildById(int id, WCFlags flags = 0) { // ThrowIfInvalid(); // if (flags.Has(WCFlags.DirectChild | WCFlags.HiddenToo)) return Api.GetDlgItem(this, id); //fast // var d = new _KidEnumData() { wThis = this, id = id }; //info: to avoid garbage delegates, we use _KidEnumData instead of captured variables // var wParent = this; // Api.EnumChildWindows(this, (c, p) => { // ref var x = ref *(_KidEnumData*)p; // if (c.ControlId == x.id) { // if (x.flags.Has(WCFlags.DirectChild) && c.ParentGWL_ != x.wThis) return 1; // if (c.IsVisibleIn_(wParent)) { x.cVisible = c; return 0; } // if (x.flags.Has(WCFlags.HiddenToo) && x.cHidden.Is0) x.cHidden = c; // } // return 1; // }, &d); // return d.cVisible.Is0 ? d.cHidden : d.cVisible; //} ///// ///// Finds a child control by its id and returns its handle as . Can wait and throw . ///// ///// Child control handle. If not found, throws exception or returns default(wnd) (if wait negative). ///// The wait timeout, seconds. If 0, does not wait. If negative, does not throw exception when not found. ///// ///// ///// This variable is invalid (window not found, closed, etc). Or closed while waiting. ///// //public wnd ChildById(Seconds wait, int id, WCFlags flags = 0) { // wnd r; // if (wait.Exists_()) { // r = ChildById(id, flags); // } else { // var to = new WaitLoop(wait); // do { r = ChildById(id, flags); if (!r.Is0) break; } while (to.Sleep()); // } // return !r.Is0 || wait.Time < 0 ? r : throw new NotFoundException(); //} //struct _KidEnumData //{ // public wnd wThis, cVisible, cHidden; // public int id; // public WCFlags flags; //} /// /// Finds a direct child control by name and/or class name and returns its handle as . /// /// Returns default(wnd) if not found. See also: . Supports . /// /// Name. /// Full, case-insensitive. Wildcard etc not supported. /// null means "can be any". "" means "no name". /// Must include the invisible '&' characters that are used to underline keyboard shortcuts with the Alt key. /// /// /// Class name. /// Full, case-insensitive. Wildcard etc not supported. /// null means "can be any". Cannot be "". /// /// If used, starts searching from the next control in the Z order. /// /// Calls API FindWindowEx. /// Faster than , which uses API EnumChildWindows. /// Can be used only when you know full name and/or class name. /// Finds hidden controls too. Finds only direct children, not other descendants. /// public wnd ChildFast(string name, string cn, wnd wAfter = default) { //ThrowIfInvalid(); //no, it can be HWND_MESSAGE if (Is0) { Api.SetLastError(Api.ERROR_INVALID_WINDOW_HANDLE); return default; } return Api.FindWindowEx(this, wAfter, cn, name); } /// /// Finds a direct child control by its id and returns its handle as . /// /// Returns default(wnd) if not found. See also: . Supports . /// Control id. /// /// Calls API GetDlgItem. /// Faster than , which uses API EnumChildWindows. /// Finds only direct children, not other descendants. Finds hidden controls too. /// Not all controls have a useful id. If control id is not unique or is different in each window instance, this function is not useful. /// public wnd ChildFast(int id) { //ThrowIfInvalid(); //no, let it be same as other overload return Api.GetDlgItem(this, id); } public partial struct getwnd { /// /// Gets child controls, including all descendants. /// /// Array containing zero or more . /// Need only visible controls. /// Place all array elements of hidden controls at the end of the array. /// Need only direct children, not all descendants. /// This variable is invalid (window not found, closed, etc). /// /// Calls API EnumChildWindows. /// /// public wnd[] Children(bool onlyVisible = false, bool sortFirstVisible = false, bool directChild = false) { _w.ThrowIfInvalid(); return Internal_.EnumWindows(Internal_.EnumAPI.EnumChildWindows, onlyVisible, sortFirstVisible, _w, directChild); } //rejected ///// ///// Gets child controls, including all descendants. ///// ///// Receives results. If null, this function creates new List, else clears before adding items. ///// Need only visible controls. ///// Place all array elements of hidden controls at the end of the array. ///// Need only direct children, not all descendants. ///// This variable is invalid (window not found, closed, etc). ///// ///// Use this overload to avoid much garbage when calling frequently with the same List variable. Other overload always allocates new array. This overload in most cases reuses memory allocated for the list variable. ///// //public void Children(ref List a, bool onlyVisible = false, bool sortFirstVisible = false, bool directChild = false) { // _w.ThrowIfInvalid(); // Internal_.EnumWindows2(Internal_.EnumAPI.EnumChildWindows, onlyVisible, sortFirstVisible, _w, directChild, list: a ??= new List()); //} //rejected: unreliable. ///// ///// Gets list of direct child controls. ///// Faster than API EnumChildWindows. ///// Should be used only with windows of current thread. Else it is unreliable because, if some controls are zordered or destroyed while enumerating, some controls can be skipped or retrieved more than once. ///// //public wnd[] DirectChildrenFastUnsafe(string cn = null) //{ // wildex wild = cn; // var a = new List(); // for(wnd c = FirstChild; !c.Is0; c = c.Next) { // if(wild != null && !c._ClassNameIs(wild)) continue; // a.Add(c); // } // return a.ToArray(); //} } ///// ///// Casts this to . ///// //public WButton AsButton => new(this); /// /// Finds and clicks a button in this window. Does not use the mouse. /// /// Control id of the button. See . /// This variable is invalid (window not found, closed, etc). /// Button not found. /// /// This function just calls and . /// Uses this code: mouse.postClick(this.Child(1, id: id)); /// /// /// /// public void ButtonClick(int id) => mouse.postClick(this.Child(1, id: id)); /// /// Finds and clicks a button in this window. Does not use the mouse. /// /// Button name. String format: [wildcard expression](xref:wildcard_expression). /// /// If true, finds/clicks as child control: mouse.postClick(this.Child(1, name, roleCN ?? "*Button*"));. /// If false, finds/clicks as UI element: this.Elm[roleCN ?? "BUTTON", name].Find(1).Invoke();. /// Default is false; it's slower but works with more windows. /// /// UI element role or control class name (if asControl true). String format: [wildcard expression](xref:wildcard_expression). Default role is "BUTTON", class name "*Button*". /// /// This function is just a shorter way to call other functions that have more options but require more code to call. If asControl true, it calls and . Else , , and . /// /// Invalid name (when starts with "***"). See , . /// This variable is invalid (window not found, closed, etc). /// Button not found. /// Failed to click. For example need to activate the window. No exception if asControl true. /// /// /// public void ButtonClick([ParamString(PSFormat.Wildex)] string name, bool asControl = false, string roleCN = null) { if (asControl) { mouse.postClick(this.Child(1, name, roleCN ?? "*Button*")); } else { this.Elm[roleCN ?? "BUTTON", name].Find(1).Invoke(); } } /// /// Posts a "menu item clicked" notification (WM_COMMAND) as if that menu item has been clicked. Does not use the mouse. /// /// Menu item id. Must be in range 1 to 0xffff. /// The menu item is in the title bar's context menu, not in the menu bar. Posts WM_SYSCOMMAND instead. /// Invalid window. /// Invalid itemId. /// /// Works only with classic menus. The drop-down menu window class name must be "#32768". Works with menu items in window menu bar, system menu and some context menus. /// Does not use the menu itself. Just posts WM_COMMAND or WM_SYSCOMMAND message. Even if a menu item with this id does not exist. /// This variable is the window that contains the menu bar or system menu. Or the drop-down menu window (class "#32768") that contains the menu item. /// public void MenuClick(int itemId, bool systemMenu = false) { if ((uint)(itemId - 1) >= 0xffff) throw new ArgumentOutOfRangeException(); ThrowIfInvalid(); var w = this; if (ClassNameIs("#32768") && miscInfo.getGUIThreadInfo(out var g, ThreadId) && !g.hwndMenuOwner.Is0) w = g.hwndMenuOwner; w.Post(systemMenu ? Api.WM_SYSCOMMAND : Api.WM_COMMAND, itemId); w.MinimalSleepIfOtherThread_(); } //rejected: use elm functions instead. ///// ///// Finds a menu item by name and posts a "menu item clicked" notification as if that menu item was clicked. Does not use the mouse. ///// Works with all standard menus and some non-standard menus. ///// ///// ///// Menu item name. ///// String format: [wildcard expression](xref:wildcard_expression). ///// ///// The menu item is in the title bar's context menu, not in the menu bar. //public void Click([ParamString(PSFormat.wildex)] string itemName, bool systemMenu = false) //{ // //} //rejected: need just 1 function. To get state, use elm. ///// ///// Click standard (classic) menu items, get state. ///// //public static class menu //{ //} } } namespace Au.Types { /// /// Flags for . /// [Flags] public enum WCFlags { /// Can find hidden controls. HiddenToo = 1, /// Skip indirect descendant controls (children of children and so on). DirectChild = 2, } #if !true //rejected. Nobody would use this when there is elm. Eg BM_CLICK is the same as elm.Invoke or elm.PostClick. For Check can use code if(!e.IsChecked) e.Invoke();. /// /// Like , but has only button, check box and radio button functions - Click, Check etc. /// See also . /// /// /// /// public struct WButton { /// /// Button handle as . /// public wnd W { get; } internal WButton(wnd w) { W = w; } /// public static implicit operator wnd(WButton b) => b.W; ///// //public static explicit operator WButton(wnd w) => new(w); /// public override string ToString() => W.ToString(); /// /// Posts a "click" message to this button control. Does not use the mouse. /// /// Use . If false (default), posts BM_CLICK message. /// This window is invalid. /// Failed. /// /// Works not with all button controls. Sometimes does not work if the window is inactive. /// Check boxes and radio buttons also are buttons. This function can click them. /// /// /// /// public void Click(bool useElm = false) { W.ThrowIfInvalid(); if (useElm) { using var e = elm.fromWindow(W, EObjid.CLIENT); //exception if failed e.Invoke(); } else { _PostBmClick(); //async if other thread, because may show a dialog. } W.MinimalSleepIfOtherThread_(); //FUTURE: sync better } void _PostBmClick() { var w = W.Window; bool workaround = !w.IsActive; if (workaround) w.Post(Api.WM_ACTIVATE, 1); //workaround for the documented BM_CLICK bug W.Post(BM_CLICK); //it sends WM_LBUTTONDOWN/UP if (workaround) w.Post(Api.WM_ACTIVATE, 0); } /// /// Checks or unchecks this check box. Does not use the mouse. /// Calls with state 0 or 1. /// /// Checks if true, unchecks if false. /// /// This window is invalid. /// Failed. /// /// Works not with all button controls. Sometimes does not work if the window is inactive. /// If this is a radio button, does not uncheck other radio buttons in its group. /// public void Check(bool on, bool useElm = false) { SetCheckState(on ? 1 : 0, useElm); } /// /// Sets checkbox state. Does not use the mouse. /// /// 0 unchecked, 1 checked, 2 indeterminate. /// Use . If false (default), posts BM_SETCHECK message and also BN_CLICKED notification to the parent window; if that is not possible, instead uses BM_CLICK message. /// Invalid state. /// This window is invalid. /// Failed. /// /// Does nothing if the check box already has the specified check state (if can get it). /// Works not with all button controls. Sometimes does not work if the window is inactive. /// If this is a radio button, does not uncheck other radio buttons in its group. /// public void SetCheckState(int state, bool useElm = false) { if (state < 0 || state > 2) throw new ArgumentOutOfRangeException(); W.ThrowIfInvalid(); int id; if (useElm || !_IsCheckbox() || (uint)((id = W.ControlId) - 1) >= 0xffff) { using var e = elm.fromWindow(W, EObjid.CLIENT); //exception if failed int k = _GetElmCheckState(e); if (k == state) return; if (useElm) e.Invoke(); else _PostBmClick(); bool clickAgain = false; switch (state) { case 0: if (k == 1) { W.MinimalSleepIfOtherThread_(); if (GetCheckState(true) == 2) clickAgain = true; else return; } break; case 1: if (k == 2) clickAgain = true; break; case 2: if (k == 0) clickAgain = true; break; } if (clickAgain) { if (useElm) e.Invoke(); else _PostBmClick(); } } else { if (state == W.Send(BM_GETCHECK)) return; W.Post(BM_SETCHECK, state); W.Get.DirectParent.Post(Api.WM_COMMAND, id, (nint)W); } W.MinimalSleepIfOtherThread_(); } /// /// Gets check state of this check box or radio button. /// Calls and returns true if it returns 1. /// public bool IsChecked(bool useElm = false) { return 1 == GetCheckState(useElm); } /// /// Gets check state of this check box or radio button. /// Returns 0 if unchecked, 1 if checked, 2 if indeterminate. Also returns 0 if this is not a button or if failed to get state. /// /// Use . If false (default) and this button has a standard checkbox style, uses API BM_GETCHECK. public int GetCheckState(bool useElm = false) { if (useElm || !_IsCheckbox()) { //info: Windows Forms controls are user-drawn and don't have one of the styles, therefore BM_GETCHECK does not work. try { //avoid exception in property-get functions using var e = elm.fromWindow(W, EObjid.CLIENT, flags: EWFlags.NoThrow); if (e == null) return 0; return _GetElmCheckState(e); } catch (Exception ex) { Debug_.Print(ex); } //CONSIDER: if fails, show warning. In all wnd property-get functions. return 0; } else { return (int)W.Send(BM_GETCHECK); } } int _GetElmCheckState(elm e) { var state = e.State; if (state.Has(EState.MIXED)) return 2; if (state.Has(EState.CHECKED)) return 1; return 0; } bool _IsCheckbox() { switch ((uint)W.Style & 15) { case BS_CHECKBOX: case BS_AUTOCHECKBOX: case BS_RADIOBUTTON: case BS_3STATE: case BS_AUTO3STATE: case BS_AUTORADIOBUTTON: return true; } return false; } internal const int BM_CLICK = 0xF5; internal const int BM_GETCHECK = 0xF0; internal const int BM_SETCHECK = 0xF1; internal const uint BS_CHECKBOX = 0x2; internal const uint BS_AUTOCHECKBOX = 0x3; internal const uint BS_RADIOBUTTON = 0x4; internal const uint BS_3STATE = 0x5; internal const uint BS_AUTO3STATE = 0x6; internal const uint BS_AUTORADIOBUTTON = 0x9; } #endif } ================================================ FILE: Au/wnd/wnd_find.cs ================================================ namespace Au { public unsafe partial struct wnd { /// /// Finds a top-level window and returns its handle as . /// /// Window handle, or default(wnd) if not found. See also: . /// /// Window name. Usually it is the title bar text. /// String format: [wildcard expression](xref:wildcard_expression). /// null means "can be any". "" means "no name". /// /// /// Window class name. /// String format: [wildcard expression](xref:wildcard_expression). /// null means "can be any". Cannot be "". /// /// /// Owner window, program or thread. Depends on argument type: ///
- owner window. Will use with level 2. ///
string - program file name, like "notepad.exe". String format: [wildcard expression](xref:wildcard_expression). Cannot be "" or path. ///
- (process id), (thread id). /// /// /// See , , , , . /// /// /// /// /// Callback function. Called for each matching window. /// It can evaluate more properties of the window and return true when they match. /// Example: also: t => !t.IsPopupWindow. /// Called after evaluating all other parameters except contains. /// /// /// Defines an object that must be in the client area of the window: ///
• UI element: or string like "name" or "e 'role' name" or "e 'role'". ///
• Child control: or string like "c 'cn' name" or "c '' name" or "c 'cn'". ///
• Image(s) or color(s): or string "image:..." (uses a with flag ). ///
• OCR text: or string "ocr:..." (uses an with flag ). /// /// /// - cn is "". To match any, use null. /// - of is "" or 0 or contains character '\\' or '/'. To match any, use null. /// - Invalid wildcard expression ("**options " or regular expression). /// /// /// To create code for this function, use tool Find window. /// /// If there are multiple matching windows, gets the first in the Z order matching window, preferring visible windows. /// /// On Windows 8 and later may skip Windows Store app Metro-style windows (on Windows 10 few such windows exist). It happens if this program does not have disableWindowFiltering = true in its manifest and is not uiAccess; to find such windows you can use . /// /// To find message-only windows use instead. /// /// /// Try to find Notepad window. Return if not found. /// /// Try to find Notepad window. Throw if not found. /// /// Wait for Notepad window max 3 seconds. Throw if not found during that time. /// /// Wait for Notepad window max 3 seconds. Return if not found during that time. /// /// Wait for Notepad window max 3 seconds. Throw if not found during that time. When found, wait max 1 s until becomes active, then activate. /// /// public static wnd find( [ParamString(PSFormat.Wildex)] string name = null, [ParamString(PSFormat.Wildex)] string cn = null, [ParamString(PSFormat.Wildex)] WOwner of = default, WFlags flags = 0, Func also = null, WContains contains = default ) => new wndFinder(name, cn, of, flags, also, contains).Find(); //rejected: single overload with last parameter double? wait. // Then in scripts almost always would need eg ' , wait: 1'. Or would need ', wait: 0' just for 'exception if not found'. /// /// Finds a top-level window and returns its handle as . Can wait and throw . /// /// Window handle. If not found, throws exception or returns default(wnd) (if wait negative). /// The wait timeout, seconds. If 0, does not wait. If negative, does not throw exception when not found. /// /// public static wnd find( Seconds wait, [ParamString(PSFormat.Wildex)] string name = null, [ParamString(PSFormat.Wildex)] string cn = null, [ParamString(PSFormat.Wildex)] WOwner of = default, WFlags flags = 0, Func also = null, WContains contains = default ) => new wndFinder(name, cn, of, flags, also, contains).Find(wait); //rejected: probably most users will not understand/use it. It's easy and more clear to create and use wndFinder instances. ///// ///// Gets arguments and result of this thread's last call to or . ///// ///// ///// wnd.wait and similar functions don't change this property. and some other functions of this library change this property because they call internally. ///// ///// ///// This example is similar to what does. ///// ///// //[field: ThreadStatic] //public static wndFinder lastFind { get; set; } //CONSIDER: add property: [field: ThreadStatic] public static wnd last { get; set; } /// /// Finds all matching windows. /// /// Array containing zero or more . /// /// The list is sorted to match the Z order, however hidden windows (when using ) and IME windows are always after visible windows. /// /// /// /// /// public static wnd[] findAll( [ParamString(PSFormat.Wildex)] string name = null, [ParamString(PSFormat.Wildex)] string cn = null, [ParamString(PSFormat.Wildex)] WOwner of = default, WFlags flags = 0, Func also = null, WContains contains = default) { var f = new wndFinder(name, cn, of, flags, also, contains); var a = f.FindAll(); //LastFind = f; return a; } /// /// Finds a top-level window and returns its handle as . /// /// Returns default(wnd) if not found. See also: . /// /// Name. /// Full, case-insensitive. Wildcard etc not supported. /// null means "can be any". "" means "no name". /// /// /// Class name. /// Full, case-insensitive. Wildcard etc not supported. /// null means "can be any". Cannot be "". /// /// Search only message-only windows. /// If used, starts searching from the next window in the Z order. /// /// Calls API FindWindowEx. /// Faster than , which uses API EnumWindows. /// Finds hidden windows too. /// Supports . /// It is not recommended to use this function in a loop to enumerate windows. It would be unreliable because window positions in the Z order can be changed while enumerating. Also then it would be slower than and . /// public static wnd findFast(string name = null, string cn = null, bool messageOnly = false, wnd wAfter = default) { return Api.FindWindowEx(messageOnly ? SpecHWND.MESSAGE : default, wAfter, cn, name); } internal struct Cached_ { wnd _w; long _time; /// /// Calls/returns and stores found and time. Returns the cached if called frequently and it's still valid. /// public wnd FindFast(string name, string cn, bool messageOnly) { long t = Environment.TickCount64; if (t - _time > 1000 || !_w.IsAlive) { lock ("x5rX3BZJrE+pOTqszh4ttQ") { if (t - _time > 1000 || !_w.IsAlive) { _w = findFast(name, cn, messageOnly); } } } _time = t; return _w; } /// /// Calls/returns callback f and stores found and time. Returns the cached if called frequently and it's still valid. /// public wnd Get(Func f) { long t = Environment.TickCount64; if (t - _time > 1000 || !_w.IsAlive) { lock ("x5rX3BZJrE+pOTqszh4ttQ") { if (t - _time > 1000 || !_w.IsAlive) { _w = f(); } } } _time = t; return _w; } } /// /// Finds a top-level window, like . If found, activates (optionally), else calls callback function and waits for the window. The callback should open the window, for example call . /// /// Window handle as . On timeout returns default(wnd) if wait < 0 (else exception). /// Callback function. See example. /// How long to wait for the window after calling the callback function. Seconds. Default 60. /// Activate the window. Default: true. /// wait time has expired (if >= 0). /// Failed to activate. /// /// run.it("notepad.exe")); /// print.it(w); /// ]]> /// /// public static wnd findOrRun( [ParamString(PSFormat.Wildex)] string name = null, [ParamString(PSFormat.Wildex)] string cn = null, [ParamString(PSFormat.Wildex)] WOwner of = default, WFlags flags = 0, Func also = null, WContains contains = default, Action run = null, Seconds? wait = default, bool activate = true) { wnd w = default; var f = new wndFinder(name, cn, of, flags, also, contains); if (f.Exists()) { w = f.Result; if (activate) w.Activate(); } else { run(); if (!f.Exists(wait ?? new(60))) return default; w = f.Result; if (activate) w._ActivateAfterRun(); } return w; } //What to do if activate true and the window started inactive? Activate immediately or wait, and how long? // Possible cases: // Starts inactive, but soon becomes active naturally. // Occasionally starts inactive and never would become active naturally, for example if the user clicked another window after starting the process and therefore OS disabled setforegroundwindow in the new process. // Always starts inactive and never becomes active naturally. // This code waits max 1 s. It's better if the window becomes active naturally (case 1), but need to activate in cases 2 and 3. // Tested many windows. Most were active after 0-10 ms, few after 11-70 ms, PowerShell after 250 ms, dotPeek after 1000 ms. // Some windows start active but soon a dialog pops up. // Also tested what happens if we activate the "slow" window without waiting. // Some are OK (eg PowerShell). Anyway, many windows render content after showing/activating. // But some windows (eg dotPeek) then soon become inactive temporarily, because the app activates another window. void _ActivateAfterRun() { if (!IsActive && !WaitFor(-1, w => w.IsActive)) { Activate(); } //note: exception if closed while waiting. As well as if fails to activate. } /// /// Opens and finds new window. Ignores old windows. Activates. /// /// Window handle as . On timeout returns default(wnd) if timeout < 0 (else exception). /// How long to wait for the window. Seconds. Can be 0 (infinite), >0 (exception on timeout) or <0 (no exception). More info: [](xref:wait_timeout). /// Callback function. Should open the window. See example. /// Activate the window. Default: true. /// timeout time has expired (if > 0). /// Failed to activate. /// /// This function isn't the same as just two statements and . It never returns a window that already existed before calling it. /// /// /// run.it(folders.Windows + @"explorer.exe"), /// 10, cn: "CabinetWClass"); /// print.it(w); /// ]]> /// /// public static wnd runAndFind(Action run, Seconds timeout, [ParamString(PSFormat.Wildex)] string name = null, [ParamString(PSFormat.Wildex)] string cn = null, [ParamString(PSFormat.Wildex)] WOwner of = default, WFlags flags = 0, Func also = null, WContains contains = default, bool activate = true) { var f = new wndFinder(name, cn); var a = f.FindAll(); run(); var loop = new WaitLoop(timeout); while (loop.Sleep()) { var w = f.Find(); if (!w.Is0 && !a.Contains(w)) { if (activate) w._ActivateAfterRun(); return w; } } return default; } /// /// Compares window name and other properties like does. /// /// true if all specified (non-null/default) properties match. /// /// Creates new and calls . /// To compare single parameter, use more lightweight code. Examples: if (w.Name.Like("* Notepad")), if (w.ClassNameIs("CabinetWClass")). /// /// /// /// /// /// public bool IsMatch( [ParamString(PSFormat.Wildex)] string name = null, [ParamString(PSFormat.Wildex)] string cn = null, [ParamString(PSFormat.Wildex)] WOwner of = default, WFlags flags = 0, Func also = null, WContains contains = default ) { var f = new wndFinder(name, cn, of, flags, also, contains); return f.IsMatch(this); } /// /// Compares window name and other properties like does. Can be specified multiple windows. /// /// 1-based index of the match, or 0 if none. /// /// /// Cache finders to improve performance when IsMatch is called multiple times. Creating all finders each time is expensive. /// /// public int IsMatch(ReadOnlySpan windows) { WFCache cache = windows.Length > 1 ? new() { CacheName = true, NoTimeout = true } : null; for (int i = 0; i < windows.Length; i++) { if (windows[i].IsMatch(this, cache)) return i + 1; } return 0; } public partial struct getwnd { /// /// Gets top-level windows. /// /// Array containing zero or more . /// /// Need only visible windows. /// Note: this function does not check whether windows are cloaked, as it is rather slow. Use if need. /// /// /// Place hidden windows at the end of the array. /// Not used when onlyVisible is true. /// /// Calls API EnumWindows. /// Although undocumented, the API retrieves most windows as in the Z order, however places IME windows (hidden) at the end. See also: ; /// The array can be bigger than you expect, because there are many invisible windows, tooltips, etc. See also . /// Skips message-only windows; use if need. /// On Windows 8 and later may skip Windows Store app Metro-style windows (on Windows 10 few such windows exist). It happens if this program does not have disableWindowFiltering = true in its manifest and is not uiAccess; to find such windows you can use . /// Tip: To get top-level and child windows in single array: var a = wnd.getwnd.root.Get.Children();. /// /// /// public static wnd[] allWindows(bool onlyVisible = false, bool sortFirstVisible = false) { return Internal_.EnumWindows(Internal_.EnumAPI.EnumWindows, onlyVisible, sortFirstVisible); } /// /// Gets top-level windows ordered as in the Z order. /// /// Array containing zero or more . /// /// Uses API GetWindow and ensures it is reliable. /// public static wnd[] allWindowsZorder() { //Algorithm to make getting all windows with GetWindow reliable: // Get all windows 2 times, and compare results. If different, wait 1-2 ms and repeat. // Still occasionally 1 window missing. // It seems, when OS is reordering windows, it removes a window from its internal array and inserts in another place not atomically. // To fix it, wait 1 ms after the first _GetWindows if results are different than previously. //Tested in stress conditions and compared with EnumWindows results. Never failed. //Speed with cold CPU: ~20% faster than EnumWindows. WeakReference> wr1 = t_awz.wr1, wr2 = t_awz.wr2; if (wr1 == null) t_awz = (wr1 = new(null), wr2 = new(null), default); if (!wr1.TryGetTarget(out var a1)) wr1.SetTarget(a1 = new(800)); if (!wr2.TryGetTarget(out var a2)) wr2.SetTarget(a2 = new(800)); int nRetry = 0; _GetWindows(a1); //the fix var hash1 = _Hash(a1); if (hash1 != t_awz.hash1) 1.ms(); g1: _GetWindows(a2); if (a1.SequenceEqual(a2)) { Debug_.PrintIf(nRetry > 50, nRetry); t_awz.hash1 = nRetry == 0 ? hash1 : _Hash(a2); return a2.ToArray(); } Math2.Swap(ref a1, ref a2); 1.ms(); nRetry++; goto g1; void _GetWindows(List a) { a.Clear(); for (var w = wnd.getwnd.top; !w.Is0; w = w.Get.Next()) a.Add(w); } Hash.MD5Result _Hash(List a) { return Hash.MD5(MemoryMarshal.AsBytes(CollectionsMarshal.AsSpan(a))); } } [ThreadStatic] static (WeakReference> wr1, WeakReference> wr2, Hash.MD5Result hash1) t_awz; /// /// Gets top-level windows of a thread. /// /// Array containing zero or more . /// /// Unmanaged thread id. /// See , . /// If 0, throws exception. If other invalid value (ended thread?), returns empty list. Supports . /// /// Need only visible windows. /// Place all array elements of hidden windows at the end of the array, even if the hidden windows are before some visible windows in the Z order. /// threadId is 0. /// /// Calls API EnumThreadWindows. /// /// public static wnd[] threadWindows(int threadId, bool onlyVisible = false, bool sortFirstVisible = false) { if (threadId == 0) throw new ArgumentException("0 threadId."); return Internal_.EnumWindows(Internal_.EnumAPI.EnumThreadWindows, onlyVisible, sortFirstVisible, threadId: threadId); } //rejected ///// Receives results. If null, this function creates new List, else clears before adding items. ///// This overload can be used to avoid much garbage when calling frequently. ///// //public static void threadWindows(ref List a, int threadId, bool onlyVisible = false, bool sortFirstVisible = false) { // if (threadId == 0) throw new ArgumentException("0 threadId."); // Internal_.EnumWindows2(Internal_.EnumAPI.EnumThreadWindows, onlyVisible, sortFirstVisible, threadId: threadId, list: a ??= new List()); //} /// /// Gets the first in Z order window of this thread. /// /// /// Skip WS_POPUP without WS_CAPTION. internal static wnd TopThreadWindow_(bool onlyVisible, bool nonPopup) { wnd r = default; Api.EnumThreadWindows(Api.GetCurrentThreadId(), (w, _) => { if (onlyVisible && !w.IsVisible) return 1; if (nonPopup) if ((w.Style & (WS.POPUP | WS.CAPTION)) == WS.POPUP) return 1; r = w; return 0; }); return r; } } /// /// Internal static functions. /// internal static partial class Internal_ { internal enum EnumAPI { EnumWindows, EnumThreadWindows, EnumChildWindows, } internal static wnd[] EnumWindows(EnumAPI api, bool onlyVisible, bool sortFirstVisible, wnd wParent = default, bool directChild = false, int threadId = 0) { using var a = EnumWindows2(api, onlyVisible, sortFirstVisible, wParent, directChild, threadId); return a.ToArray(); } /// /// This version creates much less garbage. /// The caller must dispose the returned ArrayBuilder_, unless list is not null. /// If list is not null, adds windows there (clears at first) and returns default(ArrayBuilder_). /// internal static ArrayBuilder_ EnumWindows2(EnumAPI api, bool onlyVisible, bool sortFirstVisible = false, wnd wParent = default, bool directChild = false, int threadId = 0, Func predicate = null, object predParam = default, List list = null ) { if (directChild && wParent == getwnd.root) { api = EnumAPI.EnumWindows; wParent = default; } ArrayBuilder_ ab = default; bool disposeArray = true; var d = new _EnumData { api = api, onlyVisible = onlyVisible, directChild = directChild, wParent = wParent }; try { switch (api) { case EnumAPI.EnumWindows: Api.EnumWindows(_wndEnumProc, &d); break; case EnumAPI.EnumThreadWindows: Api.EnumThreadWindows(threadId, _wndEnumProc, &d); break; case EnumAPI.EnumChildWindows: Api.EnumChildWindows(wParent, _wndEnumProc, &d); break; } int n = d.len; if (n > 0) { if (predicate != null) { n = 0; for (int i = 0; i < d.len; i++) { if (predicate((wnd)d.a[i], predParam)) d.a[n++] = d.a[i]; } } if (list != null) { list.Clear(); if (list.Capacity < n) list.Capacity = n + n / 2; } else { ab.Alloc(n, zeroInit: false, noExtra: true); } if (sortFirstVisible && !onlyVisible) { int j = 0; for (int i = 0; i < n; i++) { var w = (wnd)d.a[i]; if (!_EnumIsVisible(w, api, wParent)) continue; if (list != null) list.Add(w); else ab[j++] = w; d.a[i] = 0; } for (int i = 0; i < n; i++) { int wi = d.a[i]; if (wi == 0) continue; var w = (wnd)wi; if (list != null) list.Add(w); else ab[j++] = w; } } else if (list != null) { for (int i = 0; i < n; i++) list.Add((wnd)d.a[i]); } else { for (int i = 0; i < n; i++) ab[i] = (wnd)d.a[i]; } } disposeArray = false; return ab; } finally { MemoryUtil.Free(d.a); if (disposeArray) ab.Dispose(); } } static Api.WNDENUMPROC _wndEnumProc = (w, p) => ((_EnumData*)p)->Proc(w); struct _EnumData { public int* a; public int len; int _cap; public EnumAPI api; public bool onlyVisible, directChild; public wnd wParent; int _ownerTid; bool _ownerFound; public int Proc(wnd w) { if (api == EnumAPI.EnumChildWindows) { if (onlyVisible && !w.IsVisibleIn_(wParent)) return 1; if (directChild && w.ParentGWL_ != wParent) return 1; } else if (wParent.Is0) { if (onlyVisible && !w.IsVisible) return 1; } else { if (!_ownerFound && w == wParent) { _ownerFound = true; return 1; } if (onlyVisible && !w.IsVisible) return 1; if (!w.IsOwnedBy2_(wParent, _ownerFound ? 1 : 2, ref _ownerTid)) return 1; //if _ownerFound, still call with level 1, in case of bug "owned window is behind owner" } if (a == null) a = MemoryUtil.Alloc(_cap = onlyVisible ? 200 : 1000); else if (len == _cap) MemoryUtil.ReAlloc(ref a, _cap *= 2); a[len++] = (int)w; return 1; } //note: need this in exe manifest. Else EnumWindows skips "immersive" windows if this process is not admin/uiAccess. /* ... true */ } static bool _EnumIsVisible(wnd w, EnumAPI api, wnd wParent) => api == EnumAPI.EnumChildWindows ? w.IsVisibleIn_(wParent) : w.IsVisible; } } } namespace Au.Types { /// /// Flags of and similar functions. /// [Flags] public enum WFlags { /// /// Can find invisible windows. See . /// Use this carefully. Always use cn (class name), not just name, to avoid finding a wrong window with the same name. /// HiddenToo = 1, /// /// Can find cloaked windows. See . /// Cloaked are windows hidden not in the classic way, therefore does not detect it, but detects. For example, windows on inactive Windows 10 virtual desktops, ghost windows of inactive Windows Store apps, various hidden system windows. /// Use this carefully. Always use cn (class name), not just name, to avoid finding a wrong window with the same name. /// CloakedToo = 2, } /// /// Used with and similar functions to specify an owner of the window. /// Can be program name (like "notepad.exe"), process id (), thread id ( or ), owner window. /// public struct WOwner { readonly string _s; //program readonly int _i; //wnd, tid, pid readonly byte _what; //0 _o, 1 owner, 2 tid, 3 pid //readonly byte _ownerLevel; //rejected. Rarely used. Can use *also* instead. WOwner(string s) => _s = s; WOwner(int i, byte what) { _i = i; _what = what; } /// Program name like "notepad.exe", or null. See . public static implicit operator WOwner([ParamString(PSFormat.Wildex)] string program) => new(program); /// Owner window. See . Will use with level 2. public static implicit operator WOwner(wnd ownerWindow) => new((int)ownerWindow, 1); ///// Owner window. See . Will use with level 2. //public static implicit operator WOwner(System.Windows.DependencyObject ownerWindow) => new((int)ownerWindow.Hwnd(), 1); /// Process id. See . public static WOwner Process(int processId) => new(processId, 3); /// Thread id. See . public static WOwner Thread(int threadId) => new(threadId, 2); /// Thread id of this thread. public static WOwner ThisThread => new(Api.GetCurrentThreadId(), 2); /// /// Gets program name or process id or thread id or owner window. /// Other variables will be null/0. /// /// The value is "" or 0 or contains characters '\\' or '/' or is invalid wildcard expression. public void GetValue(out wildex program, out int pid, out int tid, out wnd owner) { program = null; pid = 0; tid = 0; owner = default; switch (_what) { case 0 when _s != null: if (_s.Length == 0) throw new ArgumentException("Program name cannot be \"\". Use null."); if (!_s.Starts("**")) { //can be regex if (_s.FindAny(@"\/") >= 0) throw new ArgumentException("Program name contains \\ or /."); if (pathname.findExtension(_s) < 0 && !wildex.hasWildcardChars(_s)) print.warning("Program name without .exe."); } program = _s; break; case 1: owner = (wnd)_i; if (owner.Is0) throw new ArgumentException("owner window 0"); break; case 2: if ((tid = _i) == 0) throw new ArgumentException("thread id 0"); break; case 3: if ((pid = _i) == 0) throw new ArgumentException("process id 0"); break; } } /// /// Returns true if nothing was assigned to this variable. /// public bool IsEmpty => _what == 0 && _s == null; } /// /// The contains parameter of and similar functions. /// Specifies text, image or other object that must be in the window. /// public struct WContains { readonly object _o; WContains(object o) => _o = o; /// public static implicit operator WContains(wndChildFinder f) => new(f); /// public static implicit operator WContains(elmFinder f) => new(f); /// public static implicit operator WContains(uiimageFinder f) => new(f); /// public static implicit operator WContains(ocrFinder f) => new(f); /// /// Converts from string to , , or . /// See . /// /// Exceptions of constructor of , , or . public static implicit operator WContains(string s) => new(_ParseString(s)); static object _ParseString(string s) { if (s.NE()) return null; string role = null, name = s; switch (s[0]) { case 'e': //"e 'role' name" or just "name" case 'c': //"c 'class' text" if (s.RxMatch(@"^. ?'(.+?)?' ?((?s).+)?$", out var m)) { role = m[1].Value; name = m[2].Value; if (s[0] == 'c') return new wndChildFinder(name, role); } break; case 'i' when s.Starts("image:"): return new uiimageFinder(s, IFFlags.WindowDC); case 'o' when s.Starts("ocr:"): return new ocrFinder(s[4..], OcrFlags.WindowDC); } return new elmFinder(role, name, flags: EFFlags.ClientArea) { ResultGetProperty = '-' }; } /// /// Gets object stored in this variable. Can be null, , , or . /// public object Value => _o; } /// /// Can be used with . /// public class WFCache { wnd _w; long _time; internal string Name, Class, Program; internal int Tid, Pid; /// /// Cache window name. /// Default: false. /// /// /// Window name is not cached by default because can be changed. Window class name and program name are always cached because cannot be changed. /// public bool CacheName { get; set; } /// /// Don't auto-clear cached properties on timeout. /// public bool NoTimeout { get; set; } internal void Begin(wnd w) { if (NoTimeout) { if (w != _w) { Clear(); _w = w; } } else { var t = Api.GetTickCount64(); if (w != _w || t - _time > 2500) { Clear(); if (w.IsAlive) { _w = w; _time = t; } } //else if(CacheName && t - _time > 100) Name = null; //no, instead let call Clear if need } } /// /// Clears all cached properties, or only name. /// /// /// Usually don't need to call this function. It is implicitly called when the variable is used with a new window. /// /// Clear only name (because it may change, unlike other cached properties). [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Clear(bool onlyName = false) { if (onlyName) Name = null; else { _w = default; _time = 0; Name = Class = Program = null; Tid = Pid = 0; } } /// /// Match invisible and cloaked windows too, even if the flags are not set (see ). /// public bool IgnoreVisibility { get; set; } } } ================================================ FILE: Au/wnd/wnd_fromxy.cs ================================================ namespace Au { public partial struct wnd { /// /// Gets visible top-level window or control from point. /// /// /// Coordinates. /// Tip: To specify coordinates relative to the right, bottom, work area or a non-primary screen, use , like in the example. /// /// /// /// Unlike API WindowFromPhysicalPoint etc, this function: does not skip disabled controls; always skips transparent control like group box if a smaller sibling is there. All this is not true with flag Raw. /// /// /// Find window at 100 200. /// /// /// Find window or control at 50 from left and 100 from bottom of the work area. /// /// public static wnd fromXY(POINT p, WXYFlags flags = 0) { bool needW = flags.Has(WXYFlags.NeedWindow); bool needC = flags.Has(WXYFlags.NeedControl); if (needW && needC) throw new ArgumentException("", "flags"); if (flags.HasAny(WXYFlags.Raw | WXYFlags.NeedWindow)) { var w = Api.WindowFromPoint(p); if (needW) return w.Window; return !needC || w.IsChild ? w : default; } else { var w = _FromXY(p, out bool isChild); return !needC || isChild ? w : default; } //info: //WindowFromPoint is the most reliable. It skips really transparent top-level windows (TL). Unfortunately it skips disabled controls (but not TL). //ChildWindowFromPointEx with CWP_SKIPINVISIBLE|CWP_SKIPTRANSPARENT skips all with WS_EX_TRANSPARENT, although without WS_EX_LAYERED they aren't actually transparent. //RealChildWindowFromPoint does not skip transparent TL. It works like ChildWindowFromPointEx(CWP_SKIPINVISIBLE). //None of the above API prefers a really visible control that is under a transparent part of a sibling control. RealChildWindowFromPoint does it only for group buttons, but not for tab controls etc. //All API skip windows that have a hole etc in window region at that point. //If same thread, WindowFromPoint uses WM_NCHITTEST+HTTRANSPARENT. Other API tested only with TL of other processes and don't use. //AccessibleObjectFromPoint too dirty, unreliable and often very slow, because sends WM_GETOBJECT etc. //IUIAutomation.FromPoint very slow. Getting window handle from it is not easy, > 0.5 ms. } /// public static wnd fromXY(int x, int y, WXYFlags flags = 0) => fromXY(new POINT(x, y), flags); //rejected: FromXY(Coord, Coord, ...). Coord makes no sense. /// /// Gets visible top-level window or control from mouse cursor position. /// More info: . /// public static wnd fromMouse(WXYFlags flags = 0) => fromXY(mouse.xy, flags); /// /// Gets descendant control from point. /// /// By default returns default(wnd) if the point is not in a child control; it depends on flags. /// X coordinate in client area or screen (if flag ScreenXY). Examples: 10, ^10 (reverse), .5f (fraction). /// Y coordinate. /// /// This variable is invalid (window not found, closed, etc). public wnd ChildFromXY(Coord x, Coord y, WXYCFlags flags = 0) { ThrowIfInvalid(); POINT p = flags.Has(WXYCFlags.ScreenXY) ? Coord.Normalize(x, y) : Coord.NormalizeInWindow(x, y, this); return _ChildFromXY(p, flags); } /// /// Gets descendant control from point. /// /// By default returns default(wnd) if the point is not in a child control; it depends on flags. /// Coordinates in client area or screen (if flag ScreenXY). /// /// This variable is invalid (window not found, closed, etc). public wnd ChildFromXY(POINT p, WXYCFlags flags = 0) { ThrowIfInvalid(); return _ChildFromXY(p, flags); } //Gets real control or window from point in screen. //The input control can be eg from WindowFromPhysicalPoint, which skips disabled and transparent controls and does not get controls under transparent siblings such as groupbox button. //In such cases this struct replaces the input control (hwnd field) with the real control (descendant or sibling). //Fully DPI-aware on Win8.1+. struct _WindowFromPoint : IDisposable { POINT _p; IntPtr _hr; public wnd hwnd; public RECT r; public _WindowFromPoint(POINT p, wnd hwndStart) { r = default; _hr = default; hwnd = hwndStart; _p = p; //rejected. This library does not support DPI-scaled windows on Win7-8; too expensive. //if (!osVersion.minWin8_1 && !Api.PhysicalToLogicalPoint(hwnd, ref _p)) _p = p; ////print.it(p, _p); //tested: on Win10 WM_NCHITTEST and GetWindowRgn use physical coords with DPI-scaled windows. } public void Dispose() { if (_hr != default) Api.DeleteObject(_hr); } public (bool inWindow, bool inClient) IsInWindow() { if (hwnd.GetWindowInfo_(out var k)) return (k.rcWindow.Contains(_p), k.rcClient.Contains(_p)); return default; } bool _IsPointVisibleIn_1(wnd c) { return c.HasStyle(WS.VISIBLE) && Api.GetWindowRect(c, out r) && r.Contains(_p); } bool _IsPointVisibleIn_2(wnd c) { if (_hr == default) _hr = Api.CreateRectRgn(0, 0, 0, 0); if (Api.GetWindowRgn(c, _hr) > 1 && !Api.PtInRegion(_hr, _p.x - r.left, _p.y - r.top)) return false; if (c.HasExStyle(WSE.LAYERED) && c.IsCloaked) return false; return true; //if(osVersion.minWin8 && c.HasExStyle(WSE.LAYERED) && Api.GetLayeredWindowAttributes(c, ... //never mind. We can get alpha 0 (probably rare), but difficult or impossible to detect whether the point is transparent because of color key (probably not so rare). //tested: WindowFromPoint skips all: region, cloaked, transparent (by alpha or colorkey). //tested: RealChildWindowFromPoint skips region and cloaked, but not transparent. //tested: UI Automation skips region and cloaked, but not transparent. //tested: AccessibleObjectFromPoint skips only cloaked. //tested: Spy++ skips none. //tested: NtUserWindowFromPoint is the same as WindowFromPoint. } public bool FindSibling() { bool found = false; if (Api.GetWindowRect(hwnd, out RECT r1)) { for (wnd c2 = hwnd; !(c2 = Api.GetWindow(c2, Api.GW_HWNDNEXT)).Is0;) { //GetWindow often is the slowest part. EnumChildWindows slower. if (_IsPointVisibleIn_1(c2)) { if (r.left >= r1.left && r.top >= r1.top && r.right <= r1.right && r.bottom <= r1.bottom && (r.right - r.left < r1.right - r1.left || r.bottom - r.top < r1.bottom - r1.top)) { if (_IsPointVisibleIn_2(c2)) { if (hwnd.SendTimeout(1000, out var ht, Api.WM_NCHITTEST, 0, Math2.MakeLparam(_p), SMTFlags.ABORTIFHUNG | SMTFlags.BLOCK) && ht != Api.HTTRANSPARENT) break; hwnd = c2; r1 = r; found = true; } } } } } return found; } public bool FindDescendant(bool directChild = false) { bool found = false; gNextGeneration: for (wnd c2 = Api.GetWindow(hwnd, Api.GW_CHILD); !c2.Is0; c2 = Api.GetWindow(c2, Api.GW_HWNDNEXT)) { if (_IsPointVisibleIn_1(c2) && _IsPointVisibleIn_2(c2)) { hwnd = c2; found = true; if (directChild) break; goto gNextGeneration; } } return found; } } /// /// Gets window or control from point. /// Returns default(wnd) if failed (unlikely). /// /// Point in screen. /// Receives true if control, false if top-level or failed. static wnd _FromXY(POINT p, out bool isChild) { wnd w = Api.WindowFromPoint(p); if (w.Is0) { isChild = false; return default; } using var k = new _WindowFromPoint(p, w); bool findSibling = w.IsChild, findDescendant = true; for (; ; ) { //find a smaller sibling fully covered by c if (findSibling) findDescendant |= k.FindSibling(); //find descendant, because: 1. WindowFromPoint does not find disabled controls. 2. The above code may change k.hwnd. if (!findDescendant) break; if (!k.FindDescendant()) break; findSibling = true; findDescendant = false; } isChild = findSibling; return k.hwnd; //note: don't use API [Real]ChildWindowFromPoint[Ex]. Bugs: // 1. Returns wrong control in DPI-scaled windows in non-primary screen with different DPI. // 2. Returns wrong control in RTL windows. } /// /// Gets descendant control from point. This can be top-level window or control. /// If there is no descendant, the return value depends on flags. /// /// Point in client area or screen (if flag ScreenXY). /// wnd _ChildFromXY(POINT p, WXYCFlags flags = 0) { if (!flags.Has(WXYCFlags.ScreenXY)) Api.ClientToScreen(this, ref p); bool directChild = flags.Has(WXYCFlags.DirectChild), orThis = flags.Has(WXYCFlags.OrThis), inside = flags.Has(WXYCFlags.Inside); using var k = new _WindowFromPoint(p, this); if (inside) { var v = k.IsInWindow(); if (!v.inWindow) return default; if (!v.inClient) return orThis ? this : default; } for (; ; ) { //find descendant if (!k.FindDescendant(directChild)) break; //find a smaller sibling fully covered by c if (!k.FindSibling()) break; if (directChild) break; } var R = k.hwnd; if (R == this) { if (!orThis || (!inside && !k.IsInWindow().inWindow)) R = default; } return R; } /// /// Gets sibling control in space: left, right, above or below. /// Returns default(wnd) if there is no sibling. /// /// /// Distance from this control (from its edge) in the specified direction. /// /// Distance in perpendicular direction, along the specified edge. Default 5. /// If direction is Left or Right, 0 is the top edge, 1 is 1 pixel down, -1 is 1 pixel up, and so on. /// If direction is Above or Below, 0 is the left edge, 1 is 1 pixel to the right, -1 is 1 pixel to the left, and so on. /// /// If at that point is a visible child or descendant of the sibling, get that child/descendant. Default false. /// This variable is invalid (window not found, closed, etc). /// /// This function is used mostly with controls, but supports top-level windows too. /// wnd _SiblingXY(_SibXY direction, int distance, int edgeOffset = 5, bool topChild = false) { ThrowIfInvalid(); wnd w = Get.DirectParent; if (!(w.Is0 ? GetRect(out RECT r) : GetRectIn(w, out r))) ThrowUseNative(); //note: most other wnd 'get' functions don't throw, but here it's better to throw. POINT p = default; switch (direction) { case _SibXY.Left: p = (r.left - distance, r.top + edgeOffset); break; case _SibXY.Right: p = (r.right + distance, r.top + edgeOffset); break; case _SibXY.Above: p = (r.left + edgeOffset, r.top - distance); break; case _SibXY.Below: p = (r.left + edgeOffset, r.bottom + distance); break; } //print.it(p); if(w.Is0) mouse.move(p); else mouse.move(w, p.x, p.y); wnd R = w.Is0 ? fromXY(p, topChild ? 0 : WXYFlags.NeedWindow) : w._ChildFromXY(p, topChild ? 0 : WXYCFlags.DirectChild); return R == this ? default : R; //cannot return self, but can return child if topChild, it's ok } enum _SibXY { Left, Right, Above, Below } wnd _SiblingXY(_SibXY direction) { ThrowIfInvalid(); wnd desktop = default; if (!this.IsChild) getwnd.desktop(out desktop, out _); var r = Rect; wnd nearest = default; int nearestDist = int.MaxValue; for (var c = Api.GetWindow(this, Api.GW_HWNDFIRST); !c.Is0; c = Api.GetWindow(c, Api.GW_HWNDNEXT)) { if (c == this) continue; if (!c.IsVisible) continue; var k = c.Rect; int dist = 0; switch (direction) { case _SibXY.Left: if (k.left >= r.left || k.bottom < r.top || k.top >= r.bottom) continue; dist = r.left - k.right; break; case _SibXY.Above: if (k.top >= r.top || k.right < r.left || k.left >= r.right) continue; dist = r.top - k.bottom; break; case _SibXY.Right: if (k.right <= r.right || k.bottom < r.top || k.top >= r.bottom) continue; dist = k.left - r.right; break; case _SibXY.Below: if (k.bottom <= r.bottom || k.right < r.left || k.left >= r.right) continue; dist = k.top - r.bottom; break; } dist = Math.Max(dist, 0); if (dist < nearestDist) { if (c.IsCloaked || c.IsMinimized || c.IsMaximized) continue; if (!desktop.Is0) if (c == desktop || c.ThreadId == desktop.ThreadId) continue; nearestDist = dist; nearest = c; } } return nearest; } } } namespace Au.Types { /// /// Flags for and . /// [Flags] public enum WXYFlags { /// /// Need top-level window. If at that point is a control, gets its top-level parent. /// Don't use together with NeedControl. /// NeedWindow = 1, /// /// Need a control (child window). Returns default(wnd) if there is no control at that point. /// Don't use together with NeedWindow. /// Without flags NeedWindow and NeedControl the function gets control or top-level window. /// NeedControl = 2, /// /// Just call API WindowFromPhysicalPoint. /// Faster, but skips disabled controls and in some cases gets transparent control like group box although a smaller visible sibling is there. /// Not used with flag NeedWindow. /// Raw = 4, } /// /// Flags for . /// [Flags] public enum WXYCFlags { /// /// If the point is in this window but not in a descendant control, return this. Default - return default(wnd). /// OrThis = 1, /// /// The point is in screen coordinates. Default - client area. /// ScreenXY = 2, /// /// Must be direct child of this. Default - any descendant. /// DirectChild = 4, /// /// If the point is not in client area, don't look for descendants; if with flag OrThis and the point is in this (non-client area), return this, else return default(wnd). /// Inside = 8, } } ================================================ FILE: Au/wnd/wnd_get.cs ================================================ namespace Au; public partial struct wnd { /// /// Gets related windows and controls. /// Use like wnd w2 = w1.Get.Owner; (here w1 is a variable). /// public getwnd Get => new(this); /// /// Static functions of this class are used to get special windows (used like wnd w = wnd.getwnd.top;) and all windows. /// Instances of this class are used to get related windows and controls, like wnd w2 = w1.Get.FirstChild; (here w1 is a variable). /// public partial struct getwnd { wnd _w; /// public getwnd(wnd wThis) => _w = wThis; #region instance /// /// Gets nearest visible sibling control to the left from this. /// /// default(wnd) if not found. /// This variable is invalid (window not found, closed, etc). /// /// This function is used mostly with controls, but supports top-level windows too. Skips maximized/minimized windows and desktop. /// public wnd SiblingLeft() => _w._SiblingXY(_SibXY.Left); //FUTURE: add these to the window tool, like navig in Delm. Also add previous/next/parent/child in Z order. /// /// Gets nearest visible sibling control to the right from this. /// /// default(wnd) if not found. /// This variable is invalid (window not found, closed, etc). /// /// This function is used mostly with controls, but supports top-level windows too. Skips maximized/minimized windows and desktop. /// public wnd SiblingRight() => _w._SiblingXY(_SibXY.Right); /// /// Gets nearest visible sibling control above this. /// /// default(wnd) if not found. /// This variable is invalid (window not found, closed, etc). /// /// This function is used mostly with controls, but supports top-level windows too. Skips maximized/minimized windows and desktop. /// public wnd SiblingAbove() => _w._SiblingXY(_SibXY.Above); /// /// Gets nearest visible sibling control to below this. /// /// default(wnd) if not found. /// This variable is invalid (window not found, closed, etc). /// /// This function is used mostly with controls, but supports top-level windows too. Skips maximized/minimized windows and desktop. /// public wnd SiblingBelow() => _w._SiblingXY(_SibXY.Below); /// /// Gets a visible sibling control to the left from this. /// /// default(wnd) if there is no sibling. /// Horizontal distance from the left of this control. /// Vertical offset from the top of this control. If negative - up. Default 5. /// If at that point is a visible child of the sibling, get that child. Default false. /// This variable is invalid (window not found, closed, etc). /// /// This function is used mostly with controls, but supports top-level windows too. /// public wnd SiblingLeft(int distance, int yOffset = 5, bool topChild = false) => _w._SiblingXY(_SibXY.Left, distance, yOffset, topChild); /// /// Gets a visible sibling control to the right from this. /// /// default(wnd) if there is no sibling. /// Horizontal distance from the right of this control. /// Vertical offset from the top of this control. If negative - up. Default 5. /// If at that point is a visible child of the sibling, get that child. Default false. /// This variable is invalid (window not found, closed, etc). /// /// This function is used mostly with controls, but supports top-level windows too. /// public wnd SiblingRight(int distance, int yOffset = 5, bool topChild = false) => _w._SiblingXY(_SibXY.Right, distance, yOffset, topChild); /// /// Gets a visible sibling control above this. /// /// default(wnd) if there is no sibling. /// Vertical distance from the top of this control. /// Horizontal offset from the left of this control. If negative - to the left. Default 5. /// If at that point is a visible child of the sibling, get that child. Default false. /// This variable is invalid (window not found, closed, etc). /// /// This function is used mostly with controls, but supports top-level windows too. /// public wnd SiblingAbove(int distance, int xOffset = 5, bool topChild = false) => _w._SiblingXY(_SibXY.Above, distance, xOffset, topChild); /// /// Gets a visible sibling control below this. /// /// default(wnd) if there is no sibling. /// Vertical distance from the bottom of this control. /// Horizontal offset from the left of this control. If negative - to the left. Default 5. /// If at that point is a visible child of the sibling, get that child. Default false. /// This variable is invalid (window not found, closed, etc). /// /// This function is used mostly with controls, but supports top-level windows too. /// public wnd SiblingBelow(int distance, int xOffset = 5, bool topChild = false) => _w._SiblingXY(_SibXY.Below, distance, xOffset, topChild); wnd _GetWindow(int dir, int skip) { if (skip < 0) return default; wnd w; for (w = _w; skip >= 0 && !w.Is0; skip--) { w = Api.GetWindow(w, dir); } return w; } /// /// Gets next sibling window or control in the Z order. /// /// default(wnd) if this is the last or if failed. /// How many next windows to skip. /// /// If this is a top-level window, gets next top-level window, else gets next control of the same direct parent. /// Calls API GetWindow(GW_HWNDNEXT). /// Supports . /// public wnd Next(int skip = 0) => _GetWindow(Api.GW_HWNDNEXT, skip); /// /// Gets previous sibling window or control in the Z order. /// /// default(wnd) if this is the first or if failed. /// How many previous windows to skip. /// /// If this is a top-level window, gets previous top-level window, else gets previous control of the same direct parent. /// Calls API GetWindow(GW_HWNDPREV). /// Supports . /// public wnd Previous(int skip = 0) => _GetWindow(Api.GW_HWNDPREV, skip); /// /// Gets the first sibling window or control in the Z order. /// If this is the first, returns this. /// /// /// If this is a top-level window, gets the first top-level window, else gets the first control of the same direct parent. /// Calls API GetWindow(this, GW_HWNDFIRST). /// Supports . /// public wnd FirstSibling => Api.GetWindow(_w, Api.GW_HWNDFIRST); /// /// Gets the last sibling window or control in the Z order. /// If this is the last, returns this, not default(wnd). /// /// /// If this is a top-level window, gets the last top-level window, else gets the last control of the same direct parent. /// Calls API GetWindow(this, GW_HWNDLAST). /// Supports . /// public wnd LastSibling => Api.GetWindow(_w, Api.GW_HWNDLAST); /// /// Gets the first direct child control in the Z order. /// /// default(wnd) if no children or if failed. /// /// Calls API GetWindow(GW_CHILD). /// Supports . /// public wnd FirstChild => Api.GetWindow(_w, Api.GW_CHILD); /// /// Gets the last direct child control in the Z order. /// /// default(wnd) if no children or if failed. /// /// Calls API GetWindow. /// Supports . /// public wnd LastChild { get { var t = Api.GetWindow(_w, Api.GW_CHILD); return t.Is0 ? t : Api.GetWindow(t, Api.GW_HWNDLAST); } } /// /// Gets a direct child control by index. /// /// default(wnd) if no children or if index is invalid or if failed. /// 0-based index of the child control in the Z order. /// /// Calls API GetWindow. /// Supports . /// public wnd Child(int index) { if (index < 0) return default; wnd c = Api.GetWindow(_w, Api.GW_CHILD); for (; index > 0 && !c.Is0; index--) c = Api.GetWindow(c, Api.GW_HWNDNEXT); return c; } /// /// Gets the owner window of this top-level window. /// /// default(wnd) if this window isn't owned or if failed. /// /// A window that has an owner window is always on top of it. /// Controls don't have an owner window. /// Supports . /// /// public wnd Owner => Api.GetWindow(_w, Api.GW_OWNER); /// /// Gets the top-level parent window of this control. /// If this is a top-level window, returns this. /// /// default(wnd) if failed. /// /// Supports . /// This function is the same as . /// public wnd Window => _w.Window; /// /// Gets the direct parent window of this control. It can be the top-level window or another control. /// /// default(wnd) if this is a top-level window or if failed. /// /// Supports . /// Unlike API GetParent, this function never returns the owner window. /// public wnd DirectParent { get { #if true var p = _w.GetWindowLong(GWL.HWNDPARENT); if (p == default) { //#if DEBUG //var p2 = Api.GetAncestor(_w, Api.GA_PARENT); //Debug.Assert(p2.Is0 || p2 == Root); //#endif return default; } lastError.clear(); var o = Api.GetWindow(_w, Api.GW_OWNER); if (o.Is0) { var ec = lastError.code; if (ec == 0) return (wnd)p; Debug.Assert(ec == Api.ERROR_INVALID_WINDOW_HANDLE); } return default; #else //the ms-recommended version, but >=6 times slower. The same IsTopLevelWindow (undocumented API). var p = Api.GetAncestor(_w, Api.GA_PARENT); if(p.Is0 || p == Root) return default; return p; #endif } } //rejected. Not much faster than our DirectParent. ///// ///// Calls API GetParent. ///// ///// ///// The API function is fast but unreliable. It can get parent or owner window, and fails in some cases. Read more in the API documentation. It is reliable only if you know that this window is a child window and has WS_CHILD style. ///// Supports . ///// //[Obsolete("Unreliable")] //public wnd GetParentApi => Api.GetParent(_w); /// /// Gets the first (in Z order) enabled window owned by this window. /// /// Return this window if there are no enabled owned windows. If false, then returns default(wnd). /// /// This window should be top-level, not child; see . /// /// Calls API GetWindow(GW_ENABLEDPOPUP). Supports . /// public wnd EnabledOwned(bool orThis = false) { var r = Api.GetWindow(_w, Api.GW_ENABLEDPOPUP); if (orThis) { if (r.Is0) r = _w; } else { if (r == _w) r = default; } return r; //MSDN doc says it returns this window if there are no owned, but actually returns 0. } /// /// Gets the most recently active window in the chain of windows owned by this window, or this window itself if there are no such windows. /// /// default(wnd) if failed. /// Can return an owner (or owner's owner and so on) of this window too. /// /// Supports . /// public wnd LastActiveOwnedOrThis(bool includeOwners = false) { var wRoot = RootOwnerOrThis(); //always use the root owner because GetLastActivePopup always returns _w if _w is owned var R = Api.GetLastActivePopup(wRoot); if (!includeOwners) { if (R != _w && wRoot != _w && !R.Is0) { for (var t = _w; !t.Is0; t = t.Get.Owner) if (t == R) return _w; } } return R; } /// /// Gets the bottom-most owner window in the chain of owner windows of this window. /// If this window is not owned, returns this window. /// /// default(wnd) if failed. /// If this is a child window, use its top-level parent window instead. /// Supports . public wnd RootOwnerOrThis(bool supportControls = false) { //return Api.GetAncestor(_w, Api.GA_ROOTOWNER); //slow, and can return Get.Root, eg for combolbox if (supportControls) { var r = Api.GetAncestor(_w, Api.GA_ROOTOWNER); return r == root ? _w : r; //never mind speed, better make it simple } else { //fast for (wnd r = _w, t; ; r = t) { t = r.Get.Owner; if (t.Is0) return r.IsAlive ? r : default; } } } /// /// Gets all owner windows (owner, its owner and so on) of this window. /// /// Add this window (or its top-level parent if control) as the first list element. /// Skip invisible windows. /// /// This window can be top-level window or control. /// public wnd[] AllOwners(bool andThisWindow = false, bool onlyVisible = false) => Owners(andThisWindow, onlyVisible).ToArray(); /// [EditorBrowsable(EditorBrowsableState.Never)] //returns List, whereas all other public functions return array public List Owners(bool andThisWindow = false, bool onlyVisible = false) { var a = new List(); for (var w = Window; !w.Is0 && w != root; w = w.Get.Owner) { if (!andThisWindow) { andThisWindow = true; continue; } if (!onlyVisible || w.IsVisible) a.Add(w); } return a; } /// /// Gets windows owned by this window. /// /// Also get indirectly owned windows (owned by owned windows). /// Add this window to the array. Since the array is sorted like in the Z order, usually it is the last element, but not always. /// Array containing zero or more elements. Sorted like in the Z order. public wnd[] AllOwned(bool allDescendants, bool andThisWindow = false) { if (allDescendants) { var od = new OwnedDescendants_(); var ai = od.GetIndices(_w, andOwner: andThisWindow); if (ai.Count == 0) return []; var ar = new wnd[ai.Count]; for (int i = 0; i < ai.Count; i++) ar[i] = od.all[ai[i]]; return ar; } else { var ar = new List(); var all = allWindowsZorder(); //the slowest part foreach (var k in all) { if (k == _w) { if (andThisWindow) ar.Add(k); continue; } if (k.Get.Owner == _w) ar.Add(k); } return ar.ToArray(); } } /// /// Gets indices of owned windows of the specified window, including all descendants. /// internal class OwnedDescendants_ { /// /// All top-level windows in Z order. /// The array is created once in ctor. /// public readonly wnd[] all; /// /// Owners of all windows that are in the all array. /// The array is created once in ctor. /// public readonly int[] owners; public OwnedDescendants_() { all = getwnd.allWindowsZorder(); owners = new int[all.Length]; for (int i = 0; i < all.Length; i++) owners[i] = (int)all[i].Get.Owner; } /// /// Gets indices of owned windows of the specified window, including all descendants. /// Can be called multiple times for different owner windows; uses arrays created in ctor (the slowest part). /// /// Owner window. /// A callback function that receives descendant index and can return true to skip that window and its descendants. /// Add owner to the list too, at the position matching the Z order. /// List of indices of owned windows. Sorted like in the Z order. Not null. public List GetIndices(wnd owner, Func skip = null, bool andOwner = false) { var ai = new List(); _Owned(owner); void _Owned(wnd owner) { int oint = (int)owner; for (int i = 0; i < owners.Length; i++) { if (owners[i] == oint) { if (skip != null && skip(i)) continue; ai.Add(i); _Owned(all[i]); } } } if (andOwner) { int j = Array.IndexOf(all, owner); if (j >= 0) ai.Add(j); else Debug_.Print("owner not in all"); } ai.Sort(); return ai; } } #endregion #region static /// /// Gets the first top-level window in the Z order. /// /// /// Probably it is a topmost window. To get the first non-topmost window, use . /// Calls API GetTopWindow. /// public static wnd top => Api.GetTopWindow(default); /// /// Finds and returns the first non-topmost window in the Z order. /// /// Receives the last topmost window. /// /// This function is slower than etc. Enumerates windows, because there is no API to get directly. /// public static wnd top2(out wnd lastTopmost) { wnd w, lastTM, w2 = default, lastTM2 = default; for (; ; ) { //repeat until gets same results 2 times. At any time windows can be destroyed, reordered, reparented. The second time 2-3 times faster. lastTM = default; w = top; if (w.Is0) break; //no windows in this session for (; w.IsTopmost; w = w.Get.Next()) lastTM = w; if (w == w2 && lastTM == lastTM2 && !w.Is0) break; w2 = w; lastTM2 = lastTM; } lastTopmost = lastTM; return w; } /// /// Calls API GetDesktopWindow. It gets the virtual parent window of all top-level windows. /// /// /// It is not the desktop window (see ) that displays icons and wallpaper. /// public static wnd root { get; } = Api.GetDesktopWindow(); /// /// Calls API GetShellWindow. It gets a window of the shell process (usually process "explorer", class name "Progman"). /// /// default(wnd) if there is no shell process, for example Explorer process killed/crashed and still not restarted, or if using a custom shell that does not register a shell window. /// /// It can be the window that contains desktop icons (see ) or other window of the same thread. /// public static wnd shellWindow => Api.GetShellWindow(); /// /// Gets the desktop window and its child control that displays desktop icons and wallpaper. /// /// false if failed. /// Receives the top-level desktop window. Class name "Progman" or "WorkerW". /// Receives the control of "SysListView32" class that contains icons and wallpaper. /// /// This function is not very reliable. May stop working on a new Windows version or don't work with a custom shell. /// Fails if there is no shell process, for example Explorer process killed/crashed and still not restarted, or if using a custom shell that does not register a shell window. /// public static bool desktop(out wnd desktopWindow, out wnd control) { wnd w = shellWindow; var f = new wndChildFinder(cn: "SysListView32"); if (!f.Exists(w)) w = wnd.find(null, "WorkerW", WOwner.Thread(w.ThreadId), also: t => f.Exists(t)); desktopWindow = w; control = f.Result; return !w.Is0; //info: //If no wallpaper, desktop is GetShellWindow, else a visible WorkerW window. //When was no wallpaper and user selects a wallpaper, explorer creates WorkerW and moves the same SysListView32 control to it. //tested: with COM (IShellWindows -> IShellBrowser -> IShellView.GetWindow) slower 17 times. } //FUTURE: //public static wnd editorWindow => //public static wnd editorCodeControl => #endregion } #region main windows public partial struct getwnd { /// /// Returns true if window w is considered a main window, ie probably is in the Windows taskbar. /// Returns false if it is invisible, cloaked, owned, toolwindow, menu, etc. /// /// A top-level window. /// On Windows 10 include (return true for) windows on all virtual desktops. On Windows 8 include Windows Store apps if possible; read more: . /// Return false if w is minimized. public static bool isMainWindow(wnd w, bool allDesktops = false, bool skipMinimized = false) { if (!w.IsVisible) return false; var exStyle = w.ExStyle; if ((exStyle & WSE.APPWINDOW) == 0) { if ((exStyle & (WSE.TOOLWINDOW | WSE.NOACTIVATE)) != 0) return false; if (!w.Get.Owner.Is0) return false; } if (skipMinimized && w.IsMinimized) return false; if (osVersion.minWin10) { if (w.IsCloaked) { if (!allDesktops) return false; if ((exStyle & WSE.NOREDIRECTIONBITMAP) != 0) { //probably a store app switch (w.ClassNameIs("Windows.UI.Core.CoreWindow", "ApplicationFrameWindow")) { case 1: return false; //Windows search, experience host, etc. Also app windows that normally would sit on ApplicationFrameWindow windows. case 2: if (_WindowsStoreAppFrameChild(w).Is0) return false; break; //skip hosts } } } } else if (osVersion.minWin8) { if ((exStyle & WSE.NOREDIRECTIONBITMAP) != 0 && !w.HasStyle(WS.CAPTION)) { if (!allDesktops && (exStyle & WSE.TOPMOST) != 0) return false; //skip store apps if (shellWindow.GetThreadProcessId(out var pidShell) != 0 && w.GetThreadProcessId(out var pid) != 0 && pid == pidShell) return false; //skip shell windows with no title bar } //On Win8 impossible to get next window like Alt+Tab. // All store apps are topmost, covering non-topmost desktop windows. // DwmGetWindowAttribute makes no sense here. // Desktop windows are never cloaked, inactive store windows are cloaked, etc. } return true; } /// /// Gets main windows, ie those that probably are in the Windows taskbar. /// /// Array containing zero or more . /// On Windows 10 include windows on all virtual desktops. On Windows 8 include Windows Store apps if possible; read more: . /// /// Uses . /// Does not match the order of buttons in the Windows taskbar. /// public static wnd[] mainWindows(bool allDesktops = false) { var a = new List(); foreach (var w in allWindows(onlyVisible: true)) { if (isMainWindow(w, allDesktops: allDesktops)) a.Add(w); } return a.ToArray(); //Another way - UI Automation: // var x = new CUIAutomation8(); // var cond = x.CreatePropertyCondition(30003, 0xC370); //UIA_ControlTypePropertyId, UIA_WindowControlTypeId // var a = x.GetRootElement().FindAll(TreeScope.TreeScope_Children, cond); // for(int i = 0; i < a.Length; i++) print.it((wnd)a.GetElement(i).CurrentNativeWindowHandle); //Advantages: 1. Maybe can filter unwanted windows more reliably, although I did't notice a difference. //Disadvantages: 1. Skips windows of higher integrity level (UAC). 2. Cannot include cloaked windows, eg those in inactive Win10 virtual desktops. 3. About 1000 times slower, eg 70 ms vs 70 mcs; cold 140 ms. } /// /// Gets next window in the Z order, skipping invisible and other windows that probably are not in the Windows taskbar. /// /// default(wnd) if there are no such windows. /// Start from this window. If default(wnd), starts from the top of the Z order. /// On Windows 10 include windows on all virtual desktops. On Windows 8 include Windows Store apps if possible; read more: . /// Skip minimized windows. /// If w is not default(wnd) and there are no matching windows after it, retry from the top of the Z order. Then can return w. /// /// Uses . /// This function is quite slow. Does not match the order of buttons in the Windows taskbar. /// public static wnd nextMain(wnd w = default, bool allDesktops = false, bool skipMinimized = false, bool retryFromTop = false) { if (w.Is0) retryFromTop = false; for (; ; ) { w = w.Is0 ? getwnd.top : w.Get.Next(); if (w.Is0) { if (retryFromTop) { retryFromTop = false; continue; } return default; } if (isMainWindow(w, allDesktops: allDesktops, skipMinimized: skipMinimized)) return w; } } } /// /// Activates next non-minimized main window, like with Alt+Tab. /// /// true if activated; false if there is no such window or if failed to activate. /// /// Uses , , . /// An alternative way - send Alt+Tab keys, but it works not everywhere. /// public static bool switchActiveWindow() { try { wnd wActive = active, wRO = wActive.Get.RootOwnerOrThis(); wnd wMain = getwnd.nextMain(wRO, skipMinimized: true, retryFromTop: true); if (!wMain.Is0 && wMain != wActive && wMain != wRO) { var wMainOrOwned = wMain.Get.LastActiveOwnedOrThis(); if (!wMainOrOwned.Is0) { //print.it(wMainOrOwned); wMainOrOwned.Activate(); return true; } } } catch (AuWndException) { } return false; //notes: //This function ignores minimized windows, because: // Impossible to get exactly the same window as Alt+Tab, which on Win10 is the most recently minimized. // Activating a random minimized window is not very useful. Maybe in the future. //The order of windows used by Alt+Tab is not the same as the Z order, especially when there are minimized windows. // We cannot get that order and have to use the Z order. It seems that the order is different on Win10 but not on Win7. //After minimizing a window its position in the Z order is undefined. // Most windows then are at the bottom when used the Minimize button or ShowWindow etc. // But some are at the top, just after the active window, for example MS Document Explorer (Win7 SDK). // Also at the top when minimized with the taskbar button. } #endregion /// /// Gets the top-level parent window of this control. /// If this is a top-level window, returns this. /// /// default(wnd) if this window is invalid. /// Supports . /// public wnd Window { get { var w = Api.GetAncestor(this, Api.GA_ROOT); if (w.Is0 && this == getwnd.root) w = this; return w; } } //This is in getwnd and here, because frequently used. /// /// Returns true if this is a child window (control), false if top-level window. /// /// /// Supports . /// Another way is w.HasStyle(WS.CHILD). It is faster but less reliable, because some top-level windows have WS_CHILD style and some child windows don't. /// /// public bool IsChild => !Get.DirectParent.Is0; /// /// Returns true if this is a child or descendant of window w. /// /// /// Calls API IsChild. /// Supports . /// public bool IsChildOf(wnd w) { return Api.IsChild(w, this); } /// /// Returns (wnd)GetWindowLong(GWL.HWNDPARENT). /// internal wnd ParentGWL_ => (wnd)GetWindowLong(GWL.HWNDPARENT); /// /// Gets the active (foreground) window. /// Calls API GetForegroundWindow. /// /// Returns default(wnd) if there is no active window; more info: . public static wnd active => Api.GetForegroundWindow(); /// /// Returns true if this window is the active (foreground) window. /// public bool IsActive => !Is0 && this == Api.GetForegroundWindow(); //FUTURE: static bool IsActiveAny(list of wnd or wndFinder). /// /// Returns true if this window is the active (foreground) window. /// If this is , returns true if there is no active window. /// internal bool IsActiveOrNoActiveAndThisIsWndRoot_ { get { if (Is0) return false; var f = Api.GetForegroundWindow(); return this == (f.Is0 ? getwnd.root : f); } } /// /// Returns true if this window is directly or indirectly owned by window w. /// /// /// /// Return true if: ///
• 0 - w is direct owner. See . ///
• 1 - w is direct or indirect owner (eg owner's owner). ///
• 2 - w is direct or indirect owner, or this is a menu-like window that looks like owned by w (same thread, topmost, etc). /// /// /// Many popup menus and similar temporary windows don't have an owner window. Instead they have topmost style. If level is 2, this function tries to detect this case. In most cases it works, but not always (depends on styles). /// public bool IsOwnedBy(wnd w, int level) { if ((uint)level > 2) throw new ArgumentOutOfRangeException(nameof(level)); int tid = 0; return IsOwnedBy2_(w, level, ref tid); } /// /// This version is used by "find window" functions. /// Uses tid to avoid getting w thread id (slow) for each window; let it be 0 initially. /// internal bool IsOwnedBy2_(wnd w, int level, ref int tid) { for (var o = Get.Owner; !o.Is0; o = o.Get.Owner) { if (o == w) return true; if (level == 0) break; } if (level > 1) { //print.it("level>1"); var es = ExStyle; if (es.Has(WSE.TOPMOST) && es.HasAny(WSE.TOOLWINDOW | WSE.NOACTIVATE) && !es.Has(WSE.APPWINDOW)) { var s = Style; if (!s.Has(WS.CAPTION) && !s.Has(WS.THICKFRAME)) switch (s & (WS.CHILD | WS.POPUP)) { case WS.POPUP: case WS.CHILD when !w.IsChild: //ComboLBox if (tid == 0) tid = w.ThreadId; //much slower than style/exstyle if (ThreadId == tid) return !w.IsTopmost || ZorderIsAbove(w); break; } } } return false; } } ================================================ FILE: Au/wnd/wnd_other.cs ================================================ namespace Au { public partial struct wnd { /// /// Sets window transparency attributes (opacity and/or transparent color). /// /// Set or remove WS_EX_LAYERED style that is required for transparency. If false, other parameters are not used. /// Opacity from 0 (completely transparent) to 255 (opaque). Does not change if null. If less than 0 or greater than 255, makes 0 or 255. /// Make pixels of this color completely transparent. Does not change if null. The alpha byte is not used. /// Don't throw exception when fails. /// /// /// Uses API SetLayeredWindowAttributes. /// On Windows 7 works only with top-level windows, not with controls. /// Fails with WPF windows (class name starts with "HwndWrapper"). /// public void SetTransparency(bool allowTransparency, int? opacity = null, ColorInt? colorKey = null, bool noException = false) { var est = ExStyle; bool layered = est.Has(WSE.LAYERED); if (allowTransparency) { uint col = 0, f = 0; byte op = 0; if (colorKey != null) { f |= 1; col = (uint)colorKey.Value.ToBGR(); } if (opacity != null) { f |= 2; op = (byte)Math.Clamp(opacity.Value, 0, 255); } if (!layered) SetExStyle(est | WSE.LAYERED, noException ? WSFlags.NoException : 0); if (!Api.SetLayeredWindowAttributes(this, col, op, f) && !noException) ThrowUseNative(); } else if (layered) { SetExStyle(est & ~WSE.LAYERED, noException ? WSFlags.NoException : 0); //tested: resets attributes, ie after adding WSE.LAYERED the window will be normal } } /// /// Gets window transparency attributes (opacity and transparency color). /// /// If this function returns true and the window has an opacity attribute, receives the opacity value 0-255, else null. /// If this function returns true and the window has a transparency color attribute, receives the color, else null. /// True if the window has transparency attributes set with or API SetLayeredWindowAttributes. Supports . /// /// Uses API GetLayeredWindowAttributes. /// public bool GetTransparency(out int? opacity, out ColorInt? colorKey) { opacity = default; colorKey = default; if (/*ExStyle.Has(WSE.LAYERED) && */Api.GetLayeredWindowAttributes(this, out uint col, out byte op, out uint f)) { if (0 != (f & 1)) colorKey = col; if (0 != (f & 2)) opacity = op; return true; } return false; } /// /// Returns true if this is a full-screen window and not desktop. /// public bool IsFullScreen => IsFullScreen_(out _); internal unsafe bool IsFullScreen_(out screen scrn) { scrn = default; if (Is0) return false; //is client rect equal to window rect (no border)? RECT r, rc, rm; r = Rect; //fast int cx = r.right - r.left, cy = r.bottom - r.top; if (cx < 400 || cy < 300) return false; //too small rc = ClientRect; //fast if (rc.right != cx || rc.bottom != cy) { if (cx - rc.right > 2 || cy - rc.bottom > 2) return false; //some windows have 1-pixel border } //covers whole screen rect? scrn = screen.of(this, SODefault.Zero); if (scrn.IsEmpty) return false; rm = scrn.Rect; if (r.left > rm.left || r.top > rm.top || r.right < rm.right || r.bottom < rm.bottom - 1) return false; //info: -1 for inactive Chrome //is it desktop? if (IsOfShellThread_) return false; if (this == getwnd.root) return false; return true; //This is the best way to test for fullscreen (FS) window. Fast. //Window and client rect was equal of almost all my tested FS windows. Except Winamp visualization. //Most FS windows are same size as screen, but some slightly bigger. //Don't look at window styles. For some FS windows they are not as should be. //Returns false if the active window is owned by a fullscreen window. This is different than appbar API interprets it. It's OK for our purposes. } /// /// Returns true if this belongs to GetShellWindow's thread (usually it is the desktop window). /// internal bool IsOfShellThread_ { get => 1 == s_isShellWindow.IsShellWindow(this); } /// /// Returns true if this belongs to GetShellWindow's process (eg a folder window, desktop, taskbar). /// internal bool IsOfShellProcess_ { get => 0 != s_isShellWindow.IsShellWindow(this); } struct _ISSHELLWINDOW { int _tidW, _tidD, _pidW, _pidD; IntPtr _w, _wDesk; //not wnd because then TypeLoadException public int IsShellWindow(wnd w) { if (w.Is0) return 0; wnd wDesk = getwnd.shellWindow; //fast if (w == wDesk) return 1; //Progman. Other window (WorkerW) may be active when desktop active. //cache because GetWindowThreadProcessId quite slow if (w.Handle != _w) { _w = w.Handle; _tidW = w.GetThreadProcessId(out _pidW); } if (wDesk.Handle != _wDesk) { _wDesk = wDesk.Handle; _tidD = wDesk.GetThreadProcessId(out _pidD); } if (_tidW == _tidD) return 1; if (_pidW == _pidD) return 2; return 0; } } static _ISSHELLWINDOW s_isShellWindow; /// /// Returns true if this window has Metro style, ie is not a classic desktop window. /// /// /// On Windows 8/8.1 most Windows Store app windows and many shell windows have Metro style. /// On Windows 10 few windows have Metro style. /// On Windows 7 there are no Metro style windows. /// /// public bool IsWindows8MetroStyle { get { if (!osVersion.minWin8) return false; if (!HasExStyle(WSE.TOPMOST | WSE.NOREDIRECTIONBITMAP) || (Style & WS.CAPTION) != 0) return false; if (ClassNameIs("Windows.UI.Core.CoreWindow")) return true; if (!osVersion.minWin10 && IsOfShellProcess_) return true; return false; //could use IsImmersiveProcess, but this is better } } /// /// On Windows 10 and later returns non-zero if this top-level window is a UWP app window: 1 if class name is "ApplicationFrameWindow", 2 if "Windows.UI.Core.CoreWindow". /// /// public int IsUwpApp { get { if (!osVersion.minWin10) return 0; if (!HasExStyle(WSE.NOREDIRECTIONBITMAP)) return 0; return ClassNameIs("ApplicationFrameWindow", "Windows.UI.Core.CoreWindow"); //could use IsImmersiveProcess, but this is better } } //This is too litle tested to be public. Tested only with WinUI 3 Controls Gallery. Also WinUI3 is still kinda experimental and rare (2021). /// /// On Windows 10 and later returns true if this top-level window is a WinUI app window (class name "WinUIDesktopWin32WindowClass"). /// internal bool IsWinUI_ { get { if (!osVersion.minWin10) return false; //if (!HasExStyle(WSE.NOREDIRECTIONBITMAP)) return 0; //only the control (cn "Microsoft.UI.Content.ContentWindowSiteBridge") has this style return ClassNameIs("WinUIDesktopWin32WindowClass"); } } /// /// If this control is (or is based on) a standard control provided by Windows, such as button or treeview, returns the control type. Else returns None. /// /// /// Sends message WM_GETOBJECT QUERYCLASSNAMEIDX. Slower than or , but can detect the base type of controls based on standard Windows controls but with a different class name. /// public WControlType CommonControlType => (WControlType)Send(Api.WM_GETOBJECT, 0, (nint)EObjid.QUERYCLASSNAMEIDX); } } namespace Au.Types { /// /// Flags for and . /// [Flags] public enum WSFlags { /// Add the specified styles and don't change others. Add = 1, /// Remove the specified styles and don't change others. Remove = 2, /// Update non-client area (frame, title bar). UpdateNonclient = 4, /// Update client area. UpdateClient = 8, /// Don't throw exception when fails. NoException = 16, } /// /// /// public enum WControlType { #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member None, Listbox = 65536, Button = 65536 + 2, Static, Edit, Combobox, Scrollbar = 65536 + 10, Status, Toolbar, Progress, Animate, Tab, Hotkey, Header, Trackbar, Listview, Updown = 65536 + 22, ToolTips = 65536 + 24, Treeview, RichEdit = 65536 + 28 #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member } } ================================================ FILE: Au/wnd/wnd_private.cs ================================================ namespace Au; public unsafe partial struct wnd { /// /// if(!IsOfThisThread) { Thread.Sleep(15); SendTimeout(1000, 0); } /// internal void MinimalSleepIfOtherThread_() { if (!IsOfThisThread) MinimalSleepNoCheckThread_(); } /// /// Thread.Sleep(15); SendTimeout(1000, 0); /// internal void MinimalSleepNoCheckThread_() { Debug.Assert(!IsOfThisThread); //perf.first(); Thread.Sleep(15); SendTimeout(1000, out _, 0); //perf.nw(); } /// /// On Win10+, if w is "ApplicationFrameWindow", returns the real app window "Windows.UI.Core.CoreWindow" hosted by w. /// If w is minimized, cloaked (eg on other desktop) or the app is starting, the "Windows.UI.Core.CoreWindow" is not its child. Then searches for a top-level window named like w. It is unreliable, but no API for this. /// Info: "Windows.UI.Core.CoreWindow" windows hosted by "ApplicationFrameWindow" belong to separate processes. All "ApplicationFrameWindow" windows belong to a single process. /// static wnd _WindowsStoreAppFrameChild(wnd w) { bool retry = false; string name; g1: if (!osVersion.minWin10 || !w.ClassNameIs("ApplicationFrameWindow")) return default; wnd c = Api.FindWindowEx(w, default, "Windows.UI.Core.CoreWindow", null); if (!c.Is0) return c; if (retry) return default; name = w.NameTL_; if (name.NE()) return default; for (; ; ) { c = Api.FindWindowEx(default, c, "Windows.UI.Core.CoreWindow", name); //I could not find API for it if (c.Is0) break; if (c.IsCloaked) return c; //else probably it is an unrelated window } retry = true; goto g1; } //not used ///// ///// The reverse of _WindowsStoreAppFrameChild. ///// //static wnd _WindowsStoreAppHost(wnd w) //{ // if(!osVersion.minWin10 || !w.ClassNameIs("Windows.UI.Core.CoreWindow")) return default; // wnd wo = w.Get.DirectParent; if(!wo.Is0 && wo.ClassNameIs("ApplicationFrameWindow")) return wo; // string s = w.GetText(false, false); if(s.NE()) return default; // return Api.FindWindow("ApplicationFrameWindow", s); //} internal static partial class Internal_ { /// /// Calls API SetProp/GetProp to set/get misc flags for a window. /// Currently unused. /// internal static class WinFlags { static readonly ushort s_atom = Api.GlobalAddAtom("Au.WFlags_"); //atom is much faster than string //note: cannot delete atom, eg in static dtor. Deletes even if currently used by a window prop, making the prop useless. internal static bool Set(wnd w, WFlags_ flags, bool? setAddRem = null) { switch (setAddRem) { case true: flags = Get(w) | flags; break; case false: flags = Get(w) & ~flags; break; } return w.Prop.Set(s_atom, (int)flags); } internal static WFlags_ Get(wnd w) { return (WFlags_)(int)w.Prop[s_atom]; } internal static WFlags_ Remove(wnd w) { return (WFlags_)(int)w.Prop.Remove(s_atom); } [Flags] internal enum WFlags_ { //these were used by elm. //ChromeYes = 1, //ChromeNo = 2, } } //internal class LastWndProps //{ // wnd _w; // long _time; // string _class, _programName, _programPath; // int _tid, _pid; // void _GetCommon(wnd w) // { // var t = perf.ms; // if(w != _w || t - _time > 100) { _w = w; _class = _programName= _programPath = null; _tid = _pid = 0; } // _time = t; // } // //internal string GetName(wnd w) { _GetCommon(w); return _name; } // internal string GetClass(wnd w) { _GetCommon(w); return _class; } // internal string GetProgram(wnd w, bool fullPath) { _GetCommon(w); return fullPath ? _programPath : _programName; } // internal int GetTidPid(wnd w, out int pid) { _GetCommon(w); pid = _pid; return _tid; } // //internal void SetName(string s) => _name = s; // internal void SetClass(string s) => _class = s; // internal void SetProgram(string s, bool fullPath) { if(fullPath) _programPath = s; else _programName = s; } // internal void SetTidPid(int tid, int pid) { _tid = tid; _pid = pid; } // [ThreadStatic] static LastWndProps _ofThread; // internal static LastWndProps OfThread => _ofThread ??= new LastWndProps(); //} /// /// Returns true if w contains a non-zero special handle value (). /// Note: SpecHWND.TOP is 0. /// public static bool IsSpecHwnd(wnd w) { int i = (int)w; return (i <= 1 && i >= -3) || i == 0xffff; } /// /// Converts object to . /// Object can contain null, , Control, or System.Windows.DependencyObject (must be in element 0 of object[]). /// Avoids loading Forms and WPF dlls when not used. /// public static wnd FromObject(object o) => o switch { null => default, wnd w => w, object[] a => _Wpf(a[0]), _ => _Control(o) }; [MethodImpl(MethodImplOptions.NoInlining)] //prevents loading Forms dlls when don't need static wnd _Control(object o) => (o as System.Windows.Forms.Control).Hwnd(); [MethodImpl(MethodImplOptions.NoInlining)] //prevents loading WPF dlls when don't need static wnd _Wpf(object o) => (o as System.Windows.DependencyObject).Hwnd(); /// /// If w is handle of a WPF element (Window, Popup, HwndHost-ed control, HwndSource.RootVisual), returns that element, else null. /// Slow if HwndHost-ed control. /// w can be default. /// public static System.Windows.FrameworkElement ToWpfElement(wnd w) { if (!w.Is0) { if (System.Windows.Interop.HwndSource.FromHwnd(w.Handle) is System.Windows.Interop.HwndSource hs) return hs.RootVisual as System.Windows.FrameworkElement; for (var p = w; !(p = p.Get.DirectParent).Is0; w = p) { if (System.Windows.Interop.HwndSource.FromHwnd(p.Handle)?.RootVisual is System.Windows.Media.Visual v) { return v.FindVisualDescendant(d => d is System.Windows.Interop.HwndHost hh && hh.Handle == w.Handle, orSelf: true) as System.Windows.FrameworkElement; //speed: 200 mcs } } } return null; } /// /// An enumerable list of for and . /// Holds ArrayBuilder_ or IEnumerator or single or none. /// Must be disposed if it is ArrayBuilder_ or IEnumerator, else disposing is optional. /// internal struct WndList_ : IDisposable { internal enum ListType { None, ArrayBuilder, Enumerator, SingleWnd } ListType _t; int _i; wnd _w; IEnumerator _en; ArrayBuilder_ _ab; internal WndList_(ArrayBuilder_ ab) { _ab = ab; _t = ListType.ArrayBuilder; } internal WndList_(IEnumerable en) { var e = en?.GetEnumerator(); if (e != null) { _en = e; _t = ListType.Enumerator; } } internal WndList_(wnd w) { if (!w.Is0) { _w = w; _t = ListType.SingleWnd; } } internal ListType Type => _t; internal bool Next(out wnd w) { w = default; switch (_t) { case ListType.ArrayBuilder: if (_i == _ab.Count) return false; w = _ab[_i++]; break; case ListType.Enumerator: if (!_en.MoveNext()) return false; w = _en.Current; break; case ListType.SingleWnd: if (_i > 0) return false; _i = 1; w = _w; break; default: return false; } return true; } public void Dispose() { switch (_t) { case ListType.ArrayBuilder: _ab.Dispose(); break; case ListType.Enumerator: _en.Dispose(); break; } } } } } ================================================ FILE: Au/wnd/wnd_wait.cs ================================================ namespace Au; public partial struct wnd { /// /// Waits until window exists or is active. /// /// Window handle. On timeout returns default(wnd) if timeout is negative; else exception. /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// The window must be the active window (), and not minimized. /// timeout time has expired (if > 0). /// /// /// Parameters etc are the same as . /// By default ignores invisible and cloaked windows. Use flags if need. /// If you have a window's variable, to wait until it is active/visible/etc use instead. /// /// /// /// Using in a WPF window with async/await. /// { /// print.it("waiting for Notepad..."); /// wnd w = await Task.Run(() => wnd.wait(-10, false, "* Notepad")); /// if(w.Is0) print.it("timeout"); else print.it(w); /// }); /// if (!b.ShowDialog()) return; /// ]]> /// /// public static wnd wait(Seconds timeout, bool active, [ParamString(PSFormat.Wildex)] string name = null, [ParamString(PSFormat.Wildex)] string cn = null, [ParamString(PSFormat.Wildex)] WOwner of = default, WFlags flags = 0, Func also = null, WContains contains = default ) => new wndFinder(name, cn, of, flags, also, contains).Wait(timeout, active); /// /// Waits until any of specified windows exists or is active. /// /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// The window must be the active window (), and not minimized. /// Specifies windows, like new("Window1"), new("Window2"). /// 1-based index and window handle. On timeout returns (0, default(wnd)) if timeout is negative; else exception. /// timeout time has expired (if > 0). /// /// By default ignores invisible and cloaked windows. Use flags if need. /// /// /// /// public static (int index, wnd w) waitAny(Seconds timeout, bool active, params wndFinder[] windows) { foreach (var f in windows) f.Result = default; WFCache cache = active && windows.Length > 1 ? new WFCache() : null; var loop = new WaitLoop(timeout); for (; ; ) { if (active) { wnd w = wnd.active; for (int i = 0; i < windows.Length; i++) { if (windows[i].IsMatch(w, cache) && !w.IsMinimized) return (i + 1, w); } } else { for (int i = 0; i < windows.Length; i++) { var f = windows[i]; if (f.Exists()) return (i + 1, f.Result); } //FUTURE: optimization: get list of windows once (Lib.EnumWindows2). // Problem: list filtering depends on wndFinder flags. Even if all finders have same flags, its easy to make bugs. } if (!loop.Sleep()) return default; } } //rejected. Not useful. Use the non-static WaitForClosed. // /// // /// Waits until window does not exist. // /// // /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). // /// Returns true. On timeout returns false if timeout is negative; else exception. // /// timeout time has expired (if > 0). // /// Exceptions of . // /// // /// Parameters etc are the same as . // /// By default ignores invisible and cloaked windows. Use flags if need. // /// If you have a window's wnd variable, to wait until it is closed use instead. // /// Examples: . // /// // public static bool waitNot(Seconds timeout, // [ParamString(PSFormat.wildex)] string name = null, // [ParamString(PSFormat.wildex)] string cn = null, // [ParamString(PSFormat.wildex)] WOwner of = default, // WFlags flags = 0, Func also = null, WContents contains = default) // { // var f = new wndFinder(name, cn, of, flags, also, contains); // return WaitNot(timeout, out _, f); // } // /// // /// Waits until window does not exist. // /// // /// // /// On timeout receives the first found matching window that exists. // /// Window properties etc. Can be string, see . // /// timeout time has expired (if > 0). // public static bool waitNot(Seconds timeout, out wnd wFound, wndFinder f) // { // wFound = default; // var to = new WaitLoop(timeout); // wnd w = default; // for(; ; ) { // if(!w.IsAlive || !f.IsMatch(w)) { //if first time, or closed (!IsAlive), or changed properties (!IsMatch) // if(!f.Exists()) { wFound = default; return true; } // wFound = w = f.Result; // } // if(!to.Sleep()) return false; // } // } //rejected. Cannot use implicit conversion string to wndFinder. //public static bool waitNot(Seconds timeout, wndFinder f) // => WaitNot(timeout, out _, f); //Not often used. It's easy with await Task.Run. Anyway, need to provide an example of similar size. //public static async Task waitAsync(Seconds timeout, string name) //{ // return await Task.Run(() => wait(timeout, name)); //} /// /// Waits for a user-defined state/condition of this window. For example active, visible, enabled, closed, contains something. /// /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// Callback function (eg lambda). It is called repeatedly, until returns a value other than default(T), for example true. /// /// Do not throw exception when the window handle is invalid or the window was closed while waiting. /// In such case the callback function must return a non-default value, like in examples with . Else exception is thrown (with a small delay) to prevent infinite waiting. /// /// Returns the value returned by the callback function. On timeout returns default(T) if timeout is negative; else exception. /// timeout time has expired (if > 0). /// The window handle is invalid or the window was closed while waiting. /// /// t.IsActive); /// print.it("active"); /// /// //wait max 30 s until window w is enabled. Exception on timeout or if closed. /// w.WaitFor(30, t => t.IsEnabled); /// print.it("enabled"); /// /// //wait until window w is closed /// w.WaitFor(0, t => !t.IsAlive, true); //same as w.WaitForClosed() /// print.it("closed"); /// /// //wait until window w is minimized or closed /// w.WaitFor(0, t => t.IsMinimized || !t.IsAlive, true); /// if(!w.IsAlive) { print.it("closed"); return; } /// print.it("minimized"); /// /// //wait until window w contains focused control classnamed "Edit" /// var c = new wndChildFinder(cn: "Edit"); /// w.WaitFor(10, t => c.Exists(t) && c.Result.IsFocused); /// print.it("control focused"); /// ]]> /// public T WaitFor(Seconds timeout, Func condition, bool dontThrowIfClosed = false) { bool wasInvalid = false; var loop = new WaitLoop(timeout); for (; ; ) { if (!dontThrowIfClosed) ThrowIfInvalid(); T r = condition(this); if (!EqualityComparer.Default.Equals(r, default)) return r; if (dontThrowIfClosed) { if (wasInvalid) ThrowIfInvalid(); wasInvalid = !IsAlive; } if (!loop.Sleep()) return default; } } /// /// Waits until this window has the specified name. /// /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// /// Window name. Usually it is the title bar text. /// String format: [wildcard expression](xref:wildcard_expression). /// /// Wait until this window does not have the specified name. /// Returns true. On timeout returns false if timeout is negative; else exception. /// timeout time has expired (if > 0). /// The window handle is invalid or the window was closed while waiting. /// Invalid wildcard expression. public bool WaitForName(Seconds timeout, [ParamString(PSFormat.Wildex)] string name, bool not = false) { wildex x = name; //ArgumentNullException, ArgumentException return WaitFor(timeout, t => x.Match(t.Name) != not); } /// /// Waits until this window is closed/destroyed or until its process ends. /// /// Timeout, seconds. Can be 0 (infinite), >0 (exception) or <0 (no exception). More info: [](xref:wait_timeout). /// Wait until the process of this window ends. /// Returns true. On timeout returns false if timeout is negative; else exception. /// timeout time has expired (if > 0). /// Failed to open process handle when waitUntilProcessEnds is true. /// /// If the window is already closed, immediately returns true. /// public bool WaitForClosed(Seconds timeout, bool waitUntilProcessEnds = false) { if (!waitUntilProcessEnds) return WaitFor(timeout, t => !t.IsAlive, true); //TODO3: if window of this thread or process... if (!IsAlive) return true; using var ph = Handle_.OpenProcess(this, Api.SYNCHRONIZE); if (ph.Is0) { var e = new AuException(0, "*open process handle"); //info: with SYNCHRONIZE can open process of higher IL if (!IsAlive) return true; throw e; } return 0 != Au.wait.forHandle(timeout, 0, ph); } } ================================================ FILE: Au/x/NuGet.md ================================================ LibreAutomate is an automation library for Windows. Mostly desktop and web UI automation. To get the most of it, install the [LibreAutomate](https://www.libreautomate.com/) app. The host program should use a manifest like [this](https://github.com/qgindi/LibreAutomate/blob/master/_/default.exe.manifest). [More info](https://www.libreautomate.com/articles/Library.html). ================================================ FILE: Au.AppHost/AppHost.cpp ================================================ #define WIN32_LEAN_AND_MEAN #define _CRT_SECURE_NO_WARNINGS #pragma warning(disable: 6386 6385 6255 6305 6011 28251) #include #include #include #include "coreclrhost.h" //min supported .NET version. Installed major must be ==, minor >=, patch any (we'll use highest found), preview any (we'll use release or highest preview). #define NETVERMAJOR 10 #define NETVERMINOR 0 #define NETVERPATCH 0 //#define NETVERPATCH 2 //see the workaround comment below #define NETVER_SUPPORT_PREVIEW 0 //eg fails with 5.0.0 preview, MethodNotFound exception /* Workaround for .NET SDK 6.0.2+ bug: WPF and winforms apps can't start if installed older .NET 6 Runtime. https://github.com/dotnet/core/issues/7176 The suggested workaround works, tested. But need to edit 4 project files, install the old SDK, build ref.db with the old SDK, maybe more. Instead let show a message box "please upgrade .NET". */ #if 0 template void Print(LPCSTR frm, Args ... args) { HWND w = FindWindowW(L"QM_Editor", nullptr); if (w == 0) return; size_t size = sizeof...(Args) > 0 ? snprintf(nullptr, 0, frm, args ...) : 0; char* buf = size > 0 ? (char*)malloc(++size) : nullptr; if (buf != nullptr) { snprintf(buf, size, frm, args ...); frm = (LPCSTR)buf; } else { if (frm == nullptr) frm = ""; size = strlen(frm) + 1; } auto u = (wchar_t*)malloc(size * 2); MultiByteToWideChar(CP_UTF8, 0, frm, (int)size, u, (int)size); SendMessage(w, WM_SETTEXT, -1, (LPARAM)u); free(u); if (buf != nullptr) free(buf); } #else #define Print __noop #endif #define lenof(x) (sizeof(x) / sizeof(*(x))) #define is32bit (sizeof(void*) == 4) void _WstringFrom(std::wstring& r, LPCWSTR w1, size_t len1, LPCWSTR w2, size_t len2) { if (len1 == (size_t)(-1)) len1 = w1 == nullptr ? 0 : wcslen(w1); if (len2 == (size_t)(-1)) len2 = w2 == nullptr ? 0 : wcslen(w2); if (len1 > 0) { r.reserve(len1 + len2); r.assign(w1, len1); if (len2 > 0) r.append(w2, len2); } else r.assign(w2, len2); } void _WstringFrom(std::wstring& r, const std::wstring& w1, LPCWSTR w2, size_t len2) { _WstringFrom(r, w1.c_str(), w1.length(), w2, len2); } void _ToUtf8(LPCWSTR w, size_t len, std::string& r) { r.resize(WideCharToMultiByte(CP_UTF8, 0, w, (int)len, nullptr, 0, nullptr, nullptr)); WideCharToMultiByte(CP_UTF8, 0, w, (int)len, &r[0], (int)r.length(), nullptr, nullptr); //note: reserve/resize does not work, because resize fills with 0 chars. It seems there is no way to avoid. } void _ToUtf8(const std::wstring& w, std::string& r) { _ToUtf8(w.c_str(), (int)w.length(), r); } int _ToUtf8(LPCWSTR w, size_t len, LPSTR utf8, size_t lenUtf8) { return WideCharToMultiByte(CP_UTF8, 0, w, (int)len, utf8, (int)lenUtf8, nullptr, nullptr); } //int _ToUtf16(LPCSTR utf8, int lenUtf8, LPWSTR utf16, int lenUtf16) { // return MultiByteToWideChar(CP_UTF8, 0, utf8, lenUtf8, utf16, lenUtf16); //} bool _StrEqualI(const std::wstring& s1, LPCWSTR s2) { int len = (int)s1.length(); return wcslen(s2) == len && 2 == CompareStringOrdinal(s1.c_str(), len, s2, len, 1); } //bool _StrEqualI(const std::wstring& s1, const std::wstring& s2) { // int len = s1.length(); // return s2.length() == len && 2 == CompareStringOrdinal(s1.c_str(), len, s2.c_str(), len, 1); //} bool _FileExists(LPCWSTR path) { return 0 == (FILE_ATTRIBUTE_DIRECTORY & GetFileAttributesW(path)); } bool _DirExists(LPCWSTR path) { DWORD att = GetFileAttributesW(path); return att != 0xffffffff && 0 != (att & FILE_ATTRIBUTE_DIRECTORY); } bool _IsProcessWow64() { #if _WIN64 return false; #else BOOL isWow = 0; return IsWow64Process(GetCurrentProcess(), &isWow) && isWow; #endif } //bool _IsProcessArm64() { //#if _M_ARM64 // return true; //#else // return false; //#endif //} LPCWSTR _ProcessPlatformString() { #if _M_ARM64 return L"arm64"; #elif _WIN64 return L"x64"; #else return L"x86"; #endif } bool _IsProcessX64andOsArm64() { #if _M_X64 static bool have, is; if (!have) { have = true; USHORT processMachine = 0, nativeMachine = 0; auto IsWow64Process2 = (BOOL(WINAPI*)(HANDLE, USHORT*, USHORT*))GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "IsWow64Process2"); if (IsWow64Process2) is = IsWow64Process2(GetCurrentProcess(), &processMachine, &nativeMachine) && nativeMachine == IMAGE_FILE_MACHINE_ARM64; } return is; #else return false; #endif } struct PATHS { std::wstring appDir, netCore, netDesktop, coreclrDll, exeName; std::string asmDll, exePath; bool isPrivateNetRuntime; BYTE isEditorOrTaskExe; //1 Au.Editor.exe, 2 Au.Task.exe }; struct VERSTRUCT { int vMinor, vPatch; bool preview; LPWSTR dirName; }; bool GetRuntimeDir(LPWSTR dotnetDir, size_t len, std::wstring& rtDir, VERSTRUCT& ver, bool desktop) { //get all compatible installed .NET versions // See C:\Downloads\runtime-master\src\installer\corehost\cli\fxr\fx_resolver.cpp std::vector a; wcscpy(dotnetDir + len, desktop ? L"\\shared\\Microsoft.WindowsDesktop.App\\*" : L"\\shared\\Microsoft.NETCore.App\\*"); //#define TESTVERSIONS #ifdef TESTVERSIONS LPCWSTR atest[] = { L"5.2.1", /*L"5.0.1",*/ L"5.0.0-preview.2", /*L"5.0.5-preview.2",*/ L"5.1.2", L"5.2.2", }; //LPCWSTR atest[] = { L"3.0.1", L"5.0.0-preview.2", L"6.0.0", }; for (int itest = 0; itest < lenof(atest); itest++) { LPWSTR s = (LPWSTR)atest[itest], s0 = s; #else WIN32_FIND_DATAW fd; HANDLE h = FindFirstFileW(dotnetDir, &fd); if (h == INVALID_HANDLE_VALUE) return false; do { LPWSTR s = fd.cFileName, s0 = s; if (*s < '1' || *s > '9' || 0 == (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) continue; #endif //Print("%S", s); int vMajor = wcstol(s, &s, 10); if (vMajor != NETVERMAJOR) continue; if (*s++ != '.' || *s < '0' || *s > '9') continue; int vMinor = wcstol(s, &s, 10); if (vMinor < NETVERMINOR) continue; //if(desktop && vMinor != ver.vMinor) continue; //no, dotnet apphost eats it if (*s++ != '.' || *s < '0' || *s > '9') continue; int vPatch = wcstol(s, &s, 10); if (vPatch < NETVERPATCH) continue; VERSTRUCT v; v.vMinor = vMinor; v.vPatch = vPatch; v.preview = s[0] == '-' && s[1] == 'p'; if (!NETVER_SUPPORT_PREVIEW && v.preview && vMinor == NETVERMINOR && v.vPatch == 0) continue; v.dirName = _wcsdup(s0); //Print("%i %i %i %i", vMajor, v.vMinor, v.vPatch, v.preview); a.push_back(v); #ifdef TESTVERSIONS } #else } while (FindNextFileW(h, &fd)); FindClose(h); #endif int n = (int)a.size(); if (n == 0) return false; //get the best match // https://learn.microsoft.com/en-us/dotnet/core/versions/selection // Like documented there, we roll forward to the nearest minor version and use the highest patch version. // But dotnet apphost doesn't, eg fails if need 3.1 but found 3.2, and even if need 3.1.2 but found 3.1.0. Only rolls forward patch version. // Tested long ago. Not tested with .NET 5. Maybe this was set in json by VS. int iBest = -1, bestMinor = -1, bestPatch = -1; for (int i = 0; i < n; i++) { int v = a[i].vMinor; if (desktop && v == ver.vMinor) { bestMinor = v; break; } if ((DWORD)v < (DWORD)bestMinor) bestMinor = v; } for (int i = 0; i < n; i++) { if (a[i].vMinor != bestMinor) continue; int v = a[i].vPatch; if (desktop && v == ver.vPatch) { bestPatch = v; iBest = i; break; } if (v > bestPatch) { bestPatch = v; iBest = i; } } if (a[iBest].preview) { //don't need this if preview is auto-uninstalled when installing release, but I still did not have a chance to test for (int i = 0; i < n; i++) { if (a[i].vMinor != bestMinor || a[i].vPatch != bestPatch) continue; //if(!a[i].preview || StrCmpLogicalW(a[i].dirName, a[iBest].dirName) > 0) iBest = i; //no, it seems old previews are auto-unisnstalled if (!a[i].preview) { iBest = i; break; } } } #ifdef TESTVERSIONS Print("BEST: %i %i %S", bestMinor, bestPatch, a[iBest].dirName); exit(0); #endif _WstringFrom(rtDir, dotnetDir, wcslen(dotnetDir) - 1, a[iBest].dirName, -1); ver = a[iBest]; for (int i = 0; i < n; i++) free(a[i].dirName); return true; } char s_asmName[800] = "\0hi7yl8kJNk+gqwTDFi7ekQ"; bool GetPaths(PATHS& p) { //https://github.com/dotnet/designs/blob/master/accepted/install-locations.md wchar_t w[1000]; size_t lenApp, lenAppDir; //get exe full path lenApp = ::GetModuleFileNameW(0, w, lenof(w) - 100); _ToUtf8(w, lenApp, p.exePath); //get appDir and exeName for (lenAppDir = lenApp; w[--lenAppDir] != '\\'; ) {} p.appDir.assign(w, lenAppDir); p.exeName = w + lenAppDir + 1; //get asmDll if (s_asmName[0] == 0) p.isEditorOrTaskExe = 2; //Au.Task.exe. Don't need p.asmDll. else { if (s_asmName[0] == 1) p.isEditorOrTaskExe = 1; //Au.Editor.exe (see project BuildEvents); else exe created from a script (see Compiler.cs > _AppHost). _ToUtf8(w, lenAppDir + 1, p.asmDll); p.asmDll += s_asmName + p.isEditorOrTaskExe; } #if _M_ARM64 auto coreclr2 = L"\\dotnetARM\\coreclr.dll"; #elif _WIN64 auto coreclr2 = L"\\dotnet\\coreclr.dll"; #else auto coreclr2 = L"\\dotnet32\\coreclr.dll"; #endif p.isPrivateNetRuntime = false; //is coreclr.dll in app dir? wcscpy(w + lenAppDir, L"\\coreclr.dll"); if (_FileExists(w)) goto gPrivate; //is in "dotnet" subdir? wcscpy(w + lenAppDir, coreclr2); if (_FileExists(w)) goto gPrivate; //is this an exeProgram launched from portable LA? Then LA dir contains coreclr.dll. if (p.isEditorOrTaskExe == 0) { auto n = GetCurrentDirectory(lenof(w), w); auto s1 = L"\\Roslyn\\.exeProgram"; const int len1 = 19; if (n >= 4 + len1 && n < lenof(w) - 50 && _wcsicmp(w + n - len1, s1) == 0) { n -= len1; wcscpy(w + n, L"\\coreclr.dll"); if (_FileExists(w)) goto gPrivate; wcscpy(w + n, coreclr2); if (_FileExists(w)) goto gPrivate; } } //get .NET root path, like "C:\Program Files\dotnet". See C:\Downloads\runtime-master\src\installer\corehost\cli\fxr_resolver.cpp. for (int i = 0; i <= 2; i++) { DWORD lenDotnet; if (i == 0) { //env var DOTNET_ROOT auto varName = _IsProcessWow64() ? L"DOTNET_ROOT(x86)" : _IsProcessX64andOsArm64() ? L"DOTNET_ROOT_X64" : L"DOTNET_ROOT"; lenDotnet = GetEnvironmentVariableW(varName, w, lenof(w)); if (lenDotnet > 0) Print("%S=%S", varName, w); } else if (i == 1) { //registry InstallLocation wchar_t key[50] = LR"(SOFTWARE\dotnet\Setup\InstalledVersions\)"; wcscat(key, _ProcessPlatformString()); HKEY hk1; if (0 != RegOpenKeyExW(HKEY_LOCAL_MACHINE, key, 0, KEY_READ | KEY_WOW64_32KEY, &hk1)) lenDotnet = 0; else { if (0 != RegQueryValueExW(hk1, L"InstallLocation", nullptr, nullptr, (LPBYTE)w, &(lenDotnet = lenof(w)))) lenDotnet = 0; else lenDotnet /= 2; RegCloseKey(hk1); } //if (lenDotnet > 0) Print("registry=%S", w); } else { //default location lenDotnet = ExpandEnvironmentStringsW(L"%ProgramFiles%\\dotnet", w, lenof(w) - 100); if (w[0] == '%') lenDotnet = 0; else if (_IsProcessX64andOsArm64()) { wcscat(w, L"\\x64"); lenDotnet += 4; } //SHGetSpecialFolderPathW //no, don't use shell apis, it's not so important. Current dotnet versions set the registry value. Dotnet apphost even uses literal "Program Files" etc string. if (lenDotnet > 0) Print("default=%S", w); } if (i > 0 && lenDotnet != 0) lenDotnet--; if (lenDotnet > 1 && w[lenDotnet - 1] == '\\') lenDotnet--; if (lenDotnet > 1 && lenDotnet < lenof(w) - 100 && _DirExists(w)) { VERSTRUCT ver; if (!GetRuntimeDir(w, lenDotnet, p.netCore, ver, false)) continue; if (!GetRuntimeDir(w, lenDotnet, p.netDesktop, ver, true)) continue; //get coreclrDll _WstringFrom(p.coreclrDll, p.netCore, L"\\coreclr.dll", -1); if (_FileExists(p.coreclrDll.c_str())) return true; } } return false; gPrivate: p.isPrivateNetRuntime = true; p.coreclrDll = w; p.netCore.assign(w, p.coreclrDll.length() - 12); p.netDesktop = p.netCore; return true; } //Gets TPA assemblies (.NET, Au, etc) from resource added from file dotnet_ref_editor.txt or dotnet_ref_task.txt. //The file is created by script "Create dotnet_ref.txt.cs". // The script parses .NET deps.json, like dotnet does it at run time. // Need to run it for each major .NET version. // The script also adds Au, and in the future maybe other omnipresent assemblies. // For Au.Editor also adds Au.Controls, and in the future maybe more. // Need to edit/run it when these dependencies change. //For Au.Editor.exe and Au.Task.exe the resource is added by BuildEvents.exe (executed as a postbuild task of Au.Editor project). //For exeProgram the resource is added by Compiler._Resources.AddTpa. void BuildTpaList(const PATHS& p, std::string& tpaList) { std::string dir; auto ri = FindResource(0, (LPCWSTR)1, (LPCWSTR)220); DWORD size = SizeofResource(0, ri); auto s = (LPCSTR)LockResource(LoadResource(0, ri)); for (LPCSTR se = s + size; s < se;) { auto ss = s; while (*ss != '|' && ss < se) ss++; if (*s == '*') { switch (*(++s)) { case 'd': _ToUtf8(p.netDesktop, dir); break; case 'c': _ToUtf8(p.netCore, dir); break; default: _ToUtf8(p.appDir, dir); break; //'a' } dir += '\\'; } else { tpaList.append(dir); tpaList.append(s, ss - s); tpaList.append(".dll;"); } s = ss + 1; } //note: don't use stringstream, it makes file size *= 2. } const char** ArgsUtf8(int& nArgs) { int na = __argc - 1; auto a = __wargv + 1; size_t size = 0; for (int i = 0; i < na; i++) size += wcslen(a[i]) * 3 + 1; auto r = (const char**)malloc(na * sizeof(LPSTR) + size); LPSTR p = (LPSTR)(r + na); for (int i = 0; i < na; i++) { int k = _ToUtf8(a[i], -1, p, size); if (k == 0) exit(-1); r[i] = p; p += k; size -= k; } nArgs = na; return r; } struct _TaskInit { const char* asmFile; const char** args; int nArgs; }; int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR pCmdLine, int nCmdShow) { #if 0 //LARGE_INTEGER c; //QueryPerformanceCounter(&c); //return c.LowPart; return 0; #else //LARGE_INTEGER t1, t2, t3, t4; QueryPerformanceCounter(&t1); PATHS p; bool pathsOK = GetPaths(p); //Print("pathsOK=%i", pathsOK); //Print("exeName=%S", p.exeName.c_str()); //Print("exePath=%s", p.exePath.c_str()); //Print("asmDll=%s", p.asmDll.c_str()); //Print("appDir=%S", p.appDir.c_str()); //Print("netCore=%S", p.netCore.c_str()); //Print("netDesktop=%S", p.netDesktop.c_str()); //Print("coreclrDll=%S", p.coreclrDll.c_str()); if (!pathsOK) { wchar_t w[200]; wsprintfW(w, L"To run this application, need to install:\r\n\r\n" L".NET %i Desktop Runtime %s\r\n\r\n" L"Would you like to download it now?", NETVERMAJOR, _ProcessPlatformString()); if (IDYES == MessageBoxW(0, w, p.exeName.c_str(), MB_ICONERROR | MB_YESNO | MB_TOPMOST | MB_SETFOREGROUND)) { AllowSetForegroundWindow(ASFW_ANY); auto ShellExecuteW = (int(WINAPI*)(HWND, LPCWSTR, LPCWSTR, LPCWSTR, LPCWSTR, INT))GetProcAddress(LoadLibraryExW(L"shell32", 0, 0), "ShellExecuteW"); //wsprintfW(w, L"https://dotnet.microsoft.com/en-us/download/dotnet/%i.%i/runtime", NETVERMAJOR, NETVERMINOR); #if true wsprintfW(w, L"https://dotnet.microsoft.com/en-us/download/dotnet/%i.%i", NETVERMAJOR, NETVERMINOR); ShellExecuteW(NULL, nullptr, w, nullptr, nullptr, SW_SHOWNORMAL); Sleep(1000); wsprintfW(w, L"In the dowload page please find and download this:\r\n\r\n.NET Desktop Runtime for Windows %s", _ProcessPlatformString()); MessageBoxW(0, w, p.exeName.c_str(), MB_ICONINFORMATION | MB_TOPMOST | MB_SETFOREGROUND); #else //rejected. It's a legacy undocumented URL. Very slow in some countries, eg China, because does not use CDN. //note: this URL is different for preview/rc (before the final version released). wsprintfW(w, L"https://aka.ms/dotnet/%i.%i/windowsdesktop-runtime-win-x%i.exe", NETVERMAJOR, NETVERMINOR, bits); //latest patch ShellExecuteW(NULL, nullptr, w, nullptr, nullptr, SW_SHOWNORMAL); #endif } return -1; } HMODULE hm = LoadLibraryExW(p.coreclrDll.c_str(), 0, LOAD_WITH_ALTERED_SEARCH_PATH); //3 ms if (hm == NULL) return -2; auto coreclr_initialize = (coreclr_initialize_ptr)GetProcAddress(hm, "coreclr_initialize"); auto coreclr_execute_assembly = (coreclr_execute_assembly_ptr)GetProcAddress(hm, "coreclr_execute_assembly"); auto coreclr_shutdown = (coreclr_shutdown_ptr)GetProcAddress(hm, "coreclr_shutdown"); void* hostHandle; unsigned int domainId; int hr; { //TRUSTED_PLATFORM_ASSEMBLIES std::string tpaList; tpaList.reserve(30000); BuildTpaList(p, tpaList); //NATIVE_DLL_SEARCH_DIRECTORIES std::wstring nd16; nd16.reserve(1000); nd16 += p.appDir; #if _M_ARM64 //these are mostly fbc, if somebody uses it. See Cpp.LoadAuNativeDll. nd16 += L"\\64\\ARM\\;"; #elif _WIN64 nd16 += L"\\64\\;"; #else nd16 += L"\\32\\;"; #endif //rejected. For NuGet packages we use another way. This would not work, because sometimes need L"\\runtimes\\win10-x64\\native\\;" etc. // nd16 += p.appDir; //#if _M_ARM64 // nd16 += L"\\runtimes\\win-arm64\\native\\;"; //#elif _WIN64 // nd16 += L"\\runtimes\\win-x64\\native\\;"; //#else // nd16 += L"\\runtimes\\win-x86\\native\\;"; //#endif if (!p.isPrivateNetRuntime) { nd16 += p.netDesktop; nd16 += L"\\;"; } nd16 += p.netCore; nd16 += L"\\;"; std::string nd8; _ToUtf8(nd16, nd8); //APP_CONTEXT_BASE_DIRECTORY std::string appDir8; _ToUtf8(p.appDir, appDir8); appDir8 += '\\'; const char* propertyKeys[] = { "TRUSTED_PLATFORM_ASSEMBLIES", "NATIVE_DLL_SEARCH_DIRECTORIES", "APP_CONTEXT_BASE_DIRECTORY", "APP_PATHS" }; const char* propertyValues[] = { tpaList.c_str(), nd8.c_str(), appDir8.c_str(), nullptr }; //Note: don't add APP_PATHS. The .NET apphost does not add it too. // .NET and Au.* assemblies are in the TPA list. // For others, on assembly resolve event we call AssemblyLoadContext.LoadFromAssemblyPath. // Cannot use both APP_PATHS and event. If a dll with same name is in APP_PATHS, .NET loads it instead. int nProp = 3; #if !true std::string ap8; if (p.isEditorOrTaskExe == 0) { ap8.assign(appDir8, 0, appDir8.length() - 1); propertyValues[nProp++] = ap8.c_str(); } //this code adds only appDir. But if using nuget's "runtimes" subdir, at first need to add its dlls to TPA (or maybe subdirs to APP_PATHS). It is difficult here. The event code handles it. #endif //Print("--- %S ---", p.exeName.c_str()); //Print("TRUSTED_PLATFORM_ASSEMBLIES:"); Print("%s", propertyValues[0]); //Print("NATIVE_DLL_SEARCH_DIRECTORIES:"); Print("%s", propertyValues[1]); //Print("APP_CONTEXT_BASE_DIRECTORY:"); Print("%s", propertyValues[2]); SetEnvironmentVariableW(L"COMPlus_legacyCorruptedStateExceptionsPolicy", L"1"); //QueryPerformanceCounter(&t2); //all above code 6 ms cold, 3.6 hot hr = coreclr_initialize(p.exePath.c_str(), "main", nProp, propertyKeys, propertyValues, &hostHandle, &domainId); if (hr < 0) { return -3; } //QueryPerformanceCounter(&t3); //22 ms cold, 16 hot } //free temp strings eg tpaList 30000 //attaching debugger? Note, this must be after coreclr_initialize, else fails to attach. STARTUPINFOW si = {}; GetStartupInfoW(&si); if (si.dwXCountChars == 1703529821) { auto he = OpenEventW(SYNCHRONIZE, false, L"Au.event.Debugger"); if (!he || 0 != WaitForSingleObject(he, 30000)) return -1; CloseHandle(he); } unsigned int ec = 0; if (p.isEditorOrTaskExe == 2) { //Au.Task.exe (task process for a script with role miniProgram) auto coreclr_create_delegate = (coreclr_create_delegate_ptr)GetProcAddress(hm, "coreclr_create_delegate"); void (STDMETHODCALLTYPE * Init)(LPWSTR, _TaskInit&) = nullptr; coreclr_create_delegate(hostHandle, domainId, "Au", "Au.More.MiniProgram_", "Init", (void**)&Init); //waits until editor asks to execute a task; it sends task info through pipe //QueryPerformanceCounter(&t4); //1 ms _TaskInit t = { }; Init(pCmdLine, t); if (t.asmFile != nullptr) { //null when editor exits hr = coreclr_execute_assembly(hostHandle, domainId, t.nArgs, t.args, t.asmFile, &ec); } } else { int nArgs = 0; const char* args0[1] = {}; const char** args = *pCmdLine != 0 ? ArgsUtf8(nArgs) : args0; hr = coreclr_execute_assembly(hostHandle, domainId, nArgs, args, p.asmDll.c_str(), &ec); //6 ms } //Print("%i %i %i", (t2.LowPart - t1.LowPart) / 10, (t3.LowPart - t2.LowPart) / 10, (t4.LowPart - t3.LowPart) / 10); //tested: AppDomain.ProcessExit event is in coreclr_shutdown. // AppDomain.UnhandledException event is in coreclr_execute_assembly, and it does not return. coreclr_shutdown(hostHandle, domainId); if (hr < 0) { return -4; } return ec; #endif } ================================================ FILE: Au.AppHost/Au.AppHost.vcxproj ================================================ Release ARM64 Release Win32 Release x64 16.0 {5DCF20C5-9BBD-44E2-96BE-323A845B99F4} Win32Proj Au.AppHost Au.AppHost 10.0 Application false v143 true Unicode Application false v143 true Unicode Application false v143 true Unicode false bin\$(Configuration)\$(Platform)\ false obj\$(Configuration)\$(Platform)\ false bin\$(Configuration)\$(Platform)\ false obj\$(Configuration)\$(Platform)\ false bin\$(Configuration)\$(Platform)\ false obj\$(Configuration)\$(Platform)\ Level3 MinSpace true true false WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) true Size MultiThreaded Windows true true true 0x200000 xcopy $(TargetPath) $(SolutionDir)_\32\ /Y Level3 true true false NDEBUG;WINDOWS;%(PreprocessorDefinitions) true Size MultiThreaded Default MinSpace Windows true true true 0x200000 xcopy $(TargetPath) $(SolutionDir)_\64\ /Y Level3 true true false NDEBUG;WINDOWS;%(PreprocessorDefinitions) true Size MultiThreaded Default MinSpace Windows true true true 0x200000 xcopy $(TargetPath) $(SolutionDir)_\64\ARM\ /Y ================================================ FILE: Au.AppHost/Au.AppHost.vcxproj.filters ================================================  ================================================ FILE: Au.AppHost/ResourceHacker.txt ================================================ [FILENAMES] Exe= 64\Au.AppHost.exe SaveAs= Au.Task.exe Log= CONSOLE [COMMANDS] -add ..\Au.AppHost\Script.ico, ICONGROUP,32512,0 -addoverwrite ..\Au.Editor\Resources\Au.manifest, MANIFEST,1,0 -add dotnet_ref.txt, 220,1,0 ================================================ FILE: Au.AppHost/coreclrhost.h ================================================ // Retrieved from https://github.com/dotnet/coreclr/blob/master/src/coreclr/hosts/inc/coreclrhost.h // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. // // APIs for hosting CoreCLR // #ifndef __CORECLR_HOST_H__ #define __CORECLR_HOST_H__ #if defined(_WIN32) && defined(_M_IX86) #define CORECLR_CALLING_CONVENTION __stdcall #else #define CORECLR_CALLING_CONVENTION #endif // For each hosting API, we define a function prototype and a function pointer // The prototype is useful for implicit linking against the dynamic coreclr // library and the pointer for explicit dynamic loading (dlopen, LoadLibrary) #define CORECLR_HOSTING_API(function, ...) \ extern "C" int CORECLR_CALLING_CONVENTION function(__VA_ARGS__); \ typedef int (CORECLR_CALLING_CONVENTION *function##_ptr)(__VA_ARGS__) // // Initialize the CoreCLR. Creates and starts CoreCLR host and creates an app domain // // Parameters: // exePath - Absolute path of the executable that invoked the ExecuteAssembly (the native host application) // appDomainFriendlyName - Friendly name of the app domain that will be created to execute the assembly // propertyCount - Number of properties (elements of the following two arguments) // propertyKeys - Keys of properties of the app domain // propertyValues - Values of properties of the app domain // hostHandle - Output parameter, handle of the created host // domainId - Output parameter, id of the created app domain // // Returns: // HRESULT indicating status of the operation. S_OK if the assembly was successfully executed // CORECLR_HOSTING_API(coreclr_initialize, const char* exePath, const char* appDomainFriendlyName, int propertyCount, const char** propertyKeys, const char** propertyValues, void** hostHandle, unsigned int* domainId); // // Shutdown CoreCLR. It unloads the app domain and stops the CoreCLR host. // // Parameters: // hostHandle - Handle of the host // domainId - Id of the domain // // Returns: // HRESULT indicating status of the operation. S_OK if the assembly was successfully executed // CORECLR_HOSTING_API(coreclr_shutdown, void* hostHandle, unsigned int domainId); // // Shutdown CoreCLR. It unloads the app domain and stops the CoreCLR host. // // Parameters: // hostHandle - Handle of the host // domainId - Id of the domain // latchedExitCode - Latched exit code after domain unloaded // // Returns: // HRESULT indicating status of the operation. S_OK if the assembly was successfully executed // CORECLR_HOSTING_API(coreclr_shutdown_2, void* hostHandle, unsigned int domainId, int* latchedExitCode); // // Create a native callable function pointer for a managed method. // // Parameters: // hostHandle - Handle of the host // domainId - Id of the domain // entryPointAssemblyName - Name of the assembly which holds the custom entry point // entryPointTypeName - Name of the type which holds the custom entry point // entryPointMethodName - Name of the method which is the custom entry point // delegate - Output parameter, the function stores a native callable function pointer to the delegate at the specified address // // Returns: // HRESULT indicating status of the operation. S_OK if the assembly was successfully executed // CORECLR_HOSTING_API(coreclr_create_delegate, void* hostHandle, unsigned int domainId, const char* entryPointAssemblyName, const char* entryPointTypeName, const char* entryPointMethodName, void** delegate); // // Execute a managed assembly with given arguments // // Parameters: // hostHandle - Handle of the host // domainId - Id of the domain // argc - Number of arguments passed to the executed assembly // argv - Array of arguments passed to the executed assembly // managedAssemblyPath - Path of the managed assembly to execute (or NULL if using a custom entrypoint). // exitCode - Exit code returned by the executed assembly // // Returns: // HRESULT indicating status of the operation. S_OK if the assembly was successfully executed // CORECLR_HOSTING_API(coreclr_execute_assembly, void* hostHandle, unsigned int domainId, int argc, const char** argv, const char* managedAssemblyPath, unsigned int* exitCode); #undef CORECLR_HOSTING_API #endif // __CORECLR_HOST_H__ ================================================ FILE: Au.Controls/Au.Controls.cs ================================================ /*/ role classLibrary define CONTROLS,IDE_LA,NO_GLOBAL,NO_DEFAULT_CHARSET_UNICODE noWarnings 1591,419,649 preBuild ..\@Au.Editor\_prePostBuild.cs outputPath %folders.Workspace%\..\Au.Editor miscFlags 1 noRef *\Au.dll pr ..\@Au\Au.cs resource resources\Generic.xaml /embedded /*/ ================================================ FILE: Au.Controls/Au.Controls.csproj ================================================  net10.0-windows true true false Au.Controls Au.Controls true ..\Au.snk true bin\Au.Controls.xml 1591;419;8981 preview true False AnyCPU embedded $(DefineConstants);CONTROLS embedded $(DefineConstants);CONTROLS ================================================ FILE: Au.Controls/KMenuCommands/KMenuCommands+.cs ================================================ using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; using System.Xml.Linq; namespace Au.Controls; public partial class KMenuCommands { /// /// Contains a method delegate and a menu item that executes it. Implements and can have one or more attached buttons etc and key/mouse shortcuts that execute it. All can be disabled/enabled with single function call. /// Also used for submenu-items (created from nested types); it allows for example to enable/disable all descendants with single function call. /// public class Command : ICommand { readonly KMenuCommands _mc; readonly Delegate _del; //null if submenu readonly CommandAttribute _ca; MenuItem _mi; bool _enabled; internal Command(KMenuCommands mc, string name, string text, MemberInfo mi, CommandAttribute ca) { _mc = mc; _enabled = true; Name = name; Text = text; _ca = ca; Keys = ca.keys; if (mi is MethodInfo k) _del = k.CreateDelegate(k.GetParameters().Length == 0 ? typeof(Action) : typeof(Action)); //if (mi is MethodInfo k) _del = k.CreateDelegate(k.GetParameters().Length == 0 ? typeof(Action) : typeof(Action)); } internal void SetMenuItem_(object text, string image, MenuItem miFactory = null) { _mi = miFactory ?? new MenuItem { Header = text }; //factory action may have set it _mi.Command = this; if (image != null && miFactory?.Icon == null) _SetImage(image); } MenuItem _Mi => _mi ?? throw new InvalidOperationException("Call FactoryParams.SetMenuItem before."); /// public MenuItem MenuItem => _mi; /// /// true if this is a submenu-item. /// public bool IsSubmenu => _del == null; /// /// Method name. If submenu-item - type name. /// Or . /// May have prefix . /// public string Name { get; } /// public override string ToString() => Name; public CommandAttribute Attribute => _ca; /// /// Menu item text. Also button text/tooltip if not set. /// public string Text { get; } /// /// Button text or tooltip. Same as menu item text but without _ for Alt-underline. /// public string ButtonText { get; set; } /// /// . /// public string ButtonTooltip { get; set; } /// /// Default or custom hotkey etc. /// public string Keys { get; private set; } /// /// Subscribes to event. /// Will propagate to copied submenus. /// Call once. /// /// Called on SubmenuOpened event. Not called for events bubbled from descendant submenus. public void OnSubmenuOpened(Action action) { Debug.Assert(_submenuOpened == null); _Mi.SubmenuOpened += _submenuOpened = (o, e) => { if (o != e.Source) return; //bubbled action((MenuItem)o); }; } RoutedEventHandler _submenuOpened; //static void _SetSubmenuOpenedEvent(MenuItem mi, Action ) /// /// Something to attach to this object. Not used by this class. /// public object Tag { get; set; } /// /// Sets properties of a button to match properties of this menu item. /// /// Button or checkbox etc. /// If menu item has image, set Content = DockPanel with image and text and dock image at this side. If null (default), sets image without text. Not used if there is no image. /// Button image element, if different than menu item image. Must not be a child of something. /// Button text, if different than menu item text. /// Don't change image. /// This is a submenu. Or called from factory action before . /// /// Sets these properties: /// - Content (image or/and text), /// - ToolTip, /// - Foreground, /// - Command (to execute same method and automatically enable/disable together), /// - Automation Name (if with image), /// - if checkable, synchronizes checked state (the button should be a ToggleButton (CheckBox or RadioButton)). /// public void CopyToButton(ButtonBase b, Dock? imageAt = null, UIElement image = null, string text = null, bool skipImage = false) { if (IsSubmenu) throw new InvalidOperationException("Submenu. Use CopyToMenu."); _ = _Mi; text ??= ButtonText; if (skipImage) { switch (b.Content) { case DockPanel dp: image = dp.Children[0]; dp.Children.Clear(); break; case UIElement ue: image = ue; break; default: image = null; break; } b.Content = null; } else { image ??= CopyImage(); } if (image == null) { b.Content = text; b.Padding = new Thickness(4, 1, 4, 2); _SetTooltip(ButtonTooltip); } else if (imageAt != null) { var v = new DockPanel(); var dock = imageAt.Value; DockPanel.SetDock(image, dock); v.Children.Add(image); var t = new TextBlock { Text = text }; if (dock == Dock.Left || dock == Dock.Right) t.Margin = new Thickness(2, -1, 2, 1); v.Children.Add(t); b.Content = v; _SetTooltip(ButtonTooltip); } else { //only image b.Content = image; _SetTooltip(ButtonTooltip ?? text); } b.Foreground = _mi.Foreground; if (image != null && !text.NE()) System.Windows.Automation.AutomationProperties.SetName(b, text); b.Command = this; if (_mi.IsCheckable) { if (b is ToggleButton tb) tb.SetBinding(ToggleButton.IsCheckedProperty, new Binding("IsChecked") { Source = _mi }); else print.warning($"Menu item {Name} is checkable, but button isn't a ToggleButton (CheckBox or RadioButton)."); } void _SetTooltip(string s) { string k = this.Keys, g = _mi.InputGestureText; if (!k.NE() || !g.NE()) { if (!g.NE()) k = k.NE() ? g : g + ", " + k; s = s.NE() ? k : $"{s}\n\n{k}"; } b.ToolTip = s; } } //public void CopyToButton(out T b, Dock? imageAt = null) where T : ButtonBase, new() => CopyToButton(b = new T(), imageAt); /// /// Sets properties of another menu item (not in this menu) to match properties of this menu item. /// If this is a submenu-item, copies with descendants. /// /// /// Image element (), if different. Must not be a child of something. /// Text (), if different. /// Called from factory action before . /// /// Sets these properties: /// - Header (if string), /// - Icon (if possible), /// - InputGestureText, /// - ToolTip, /// - Foreground, /// - Command (to execute same method and automatically enable/disable together), /// - IsCheckable (and synchronizes checked state). /// public void CopyToMenu(MenuItem m, UIElement image = null, object text = null) => _CopyToMenu(_Mi, m, image, text); static MenuItem _CopyToMenu(MenuItem from, MenuItem to, UIElement image = null, object text = null) { if (from.Command is not Command c) return null; to ??= new(); to.Icon = image ?? _CopyImage(from); if (text != null) to.Header = text; else if (from.Header is string s) { if (to.Role is MenuItemRole.TopLevelItem) s = StringUtil.RemoveUnderlineChar(s, '_'); //eg _Find would disable normal operation of _File to.Header = s; } to.InputGestureText = from.InputGestureText; to.ToolTip = from.ToolTip; to.Foreground = from.Foreground; to.Command = c; bool checkable = from.IsCheckable; to.IsCheckable = checkable; if (checkable) to.SetBinding(MenuItem.IsCheckedProperty, new Binding("IsChecked") { Source = from }); if (from.HasItems) { //lazy. Toolbar item submenus created now don't display hotkeys, because keyboard bindings are set afterwards. to.Items.Add(""); RoutedEventHandler smo = null; smo = (_, _) => { to.SubmenuOpened -= smo; to.Items.Clear(); _CopyDescendants(from, to); }; to.SubmenuOpened += smo; if (c._submenuOpened != null) to.SubmenuOpened += c._submenuOpened; } return to; } static void _CopyDescendants(ItemsControl from, ItemsControl to) { int n = 0; foreach (var v in from.Items) { object k; switch (v) { case Separator: k = new Separator(); break; case MenuItem g: k = _CopyToMenu(g, null); if (k == null) continue; //not Command. Added dynamically. Will add again. break; default: continue; } to.Items.Add(k); n++; } } /// /// Copies descendants of this submenu to a context menu. /// /// This is not a submenu. Or called from factory action before . /// /// For each new item sets the same properties as other overload. /// public void CopyToMenu(ContextMenu cm) { if (!IsSubmenu) throw new InvalidOperationException("Not submenu"); _CopyDescendants(_Mi, cm); } /// /// Copies menu item image element. Returns null if no image or cannot copy. /// /// Called from factory action before . public UIElement CopyImage() => _CopyImage(_Mi); static UIElement _CopyImage(MenuItem from) { switch (from.Icon) { case Image im: return new Image { Source = im.Source }; case UIElement e when e.Uid is string res: //see _SetImage if (ResourceUtil.HasResourcePrefix(res)) return ResourceUtil.GetXamlObject(res) as UIElement; if (res.Starts("source:")) return ImageUtil.LoadWpfImageElement(res[7..]); break; } return null; } bool _SetImage(string image, bool custom = false) { try { if (image.NE()) { _mi.Icon = null; } else { #if DEBUG bool res = !(custom || image.Starts('*') || pathname.isFullPath(image)); #else bool res = !(custom || image.Starts('*')); #endif var ie = res ? ResourceUtil.GetWpfImageElement(image) : ImageUtil.LoadWpfImageElement(image); if (ie is not Image) ie.Uid = (res ? "resource:" : "source:") + image; //xaml source for _CopyImage _mi.Icon = ie; } return true; } catch (Exception ex) { if (custom) CustomizingError("failed to load image", ex); else print.it($"Failed to load image {image}. {ex.ToStringWithoutStack()}"); } return false; } /// /// Gets enabled/disabled state of this command, menu item and all controls with Command property = this (see , ). /// public bool Enabled => _enabled; /// /// Sets enabled/disabled state of this command, menu item and all controls with Command property = this (see , ). /// If submenu-item, also enables/disables all descendants. Does not actually disable/enable the submenu-item. /// /// Called from factory action before . public void Enable(bool enable) { _ = _Mi; if (enable == _enabled) return; _enabled = enable; if (IsSubmenu) { foreach (var v in _mi.Items) if (v is MenuItem m && m.Command is Command c && !c.NoIndirectDisable) c.Enable(enable); } else { CanExecuteChanged?.Invoke(this, EventArgs.Empty); //enables/disables this menu item and all buttons etc with Command=this } } /// /// Don't change the enabled state indirectly when changing that of the parent menu. /// public bool NoIndirectDisable { get; set; } /// /// Gets or sets checked state of this checkable menu item and all checkable controls with Command property = this (see , ). /// /// Called from factory action before . public bool Checked { get => _Mi.IsChecked; set { if (value != _Mi.IsChecked) _mi.IsChecked = value; } } #region ICommand public bool CanExecute(object parameter) => _enabled; public void Execute(object parameter) { _mc.ExecutingStartedEnded?.Invoke(true); try { switch (_del) { case Action a0: a0(); break; case Action a1: a1(_mi); break; //case Action a1: a1(parameter); break; //default: throw new InvalidOperationException("Submenu"); } } finally { _mc.ExecutingStartedEnded?.Invoke(false); } } /// /// When disabled or enabled with . /// public event EventHandler CanExecuteChanged; #endregion /// /// Finds and returns toolbar button that has this command. Returns null if not found. /// public ButtonBase FindButtonInToolbar(ToolBar tb) => tb.Items.OfType().FirstOrDefault(o => o.Command == this); /// /// Finds and returns toolbar menu-button that has this command. Returns null if not found. /// public MenuItem FindMenuButtonInToolbar(ToolBar tb) { foreach (var e in tb.Items) { if (e is Decorator d && d.Child is Menu m && m.Items[0] is MenuItem mi && mi.Command == this) return mi; } return null; } //public void Test() { // foreach (var v in CanExecuteChanged.GetInvocationList()) { // print.it(v.Target); // } //} internal void Customize_(XElement x, ToolBar toolbar) { OverflowMode hide = default; bool separator = false; string text = null, btext = null; Dock? imageAt = null; foreach (var a in x.Attributes()) { string an = a.Name.LocalName, av = a.Value; try { switch (an) { case "keys": Keys = av; break; case "color": _mi.Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString(av)); break; case "image": _SetImage(av, custom: true); break; case "text": _mi.Header = text = av; break; case "btext" when toolbar != null: btext = av; break; case "separator" when toolbar != null: separator = true; break; case "hide" when toolbar != null: hide = Enum.Parse(av, true); break; case "imageAt" when toolbar != null: imageAt = Enum.Parse(av, true); break; default: CustomizingError($"attribute '{an}' can't be used here"); break; } } catch (Exception ex) { CustomizingError($"invalid '{an}' value", ex); } } if ((btext ?? text) != null) ButtonText = btext ?? StringUtil.RemoveUnderlineChar(text, '_'); if (toolbar != null) { try { if (separator) { var sep = new Separator(); if (hide != default) ToolBar.SetOverflowMode(sep, hide); toolbar.Items.Add(sep); } FrameworkElement e; if (IsSubmenu) { var k = new MenuItem(); var image = _mi.Icon; bool onlyImage = image != null && imageAt == null; if (image == null || onlyImage) k.Padding = new Thickness(3, 1, 3, 2); //make taller. If image+text, button too tall, text too high, icon too low, never mind. Never mind: not good on Win7. CopyToMenu(k, text: btext); if (onlyImage) { k.Header = k.Icon; k.Icon = null; } //make narrower if (ButtonTooltip != null) k.ToolTip = ButtonTooltip; else if (onlyImage) k.ToolTip = ButtonText + "..."; var m = new Menu { Background = toolbar.Background, IsMainMenu = false, UseLayoutRounding = true }; m.Items.Add(k); //parent must be Menu, else wrong Role (must be TopLevelHeader, we can't change) and does not work //workaround for: 1. Descendant icon part black when checked. 2. Different drop-down menu style. // Never mind: different hot style. e = new Border { Child = m }; k.Padding = osVersion.minWin8 ? new(4, 2, 4, 2) : new(5, 3, 5, 3); //on Win7 smaller } else { var b = _mi.IsCheckable ? (ButtonBase)new CheckBox() : new Button(); //rejected: support RadioButton b.Focusable = false; b.UseLayoutRounding = true; CopyToButton(b, imageAt); b.Padding = new(4, 2, 4, 2); e = b; } if (hide != default) ToolBar.SetOverflowMode(e, hide); toolbar.Items.Add(e); } catch (Exception ex) { CustomizingError("failed to create button", ex); } } } public void CustomizingError(string s, Exception ex = null) { _mc.OnCustomizingError?.Invoke(this, s, ex); } } public event Action ExecutingStartedEnded; class _XElementNameEqualityComparer : IEqualityComparer { bool IEqualityComparer.Equals(XElement x, XElement y) => x.Name == y.Name; int IEqualityComparer.GetHashCode(XElement x) => x.Name.GetHashCode(); } static _XElementNameEqualityComparer s_xmlNameComparer = new(); /// /// Called on error in a custom attribute. /// public Action OnCustomizingError; /// /// Parameters for factory action of . /// public class FactoryParams { internal FactoryParams(Command command, MemberInfo member) { this.command = command; this.member = member; } /// /// The new command. /// is still null and you can call . /// public readonly Command command; /// /// of method or of nested class. /// For example allows to get attributes of any type. /// public readonly MemberInfo member; /// /// Text or a WPF element to add to the text part of the menu item. In/out parameter. /// Text may contain _ for Alt-underline, whereas command.Text is without it. /// public object text; /// . In/out parameter. public string image; /// . In/out parameter. This class does not use it. public object param; /// /// Sets property. /// If your factory action does not call this function, the menu item will be created after it returns. /// /// Your created menu item. If null, this function creates standard menu item. /// /// Uses the text and image fields; you can change them before. Sets menu item's Icon property if image!=null and mi?.Image==null. Sets Header property only if creates new item. /// The menu item will be added to the parent menu after your factory action returns. /// public void SetMenuItem(MenuItem mi = null) => command.SetMenuItem_(text, image, mi); } } /// /// Used with . /// Allows to add menu items in the same order as methods and nested types, and optionally specify menu item text etc. /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] public class CommandAttribute : Attribute { internal readonly int order_; /// /// Command name to use instead of method/type name. Use to resolve duplicate name conflict. /// public string name; /// /// Name prefix of this command, and default prefix of descendants. Use to resolve duplicate name conflict. /// public string namePrefix; /// /// Menu item text. Use _ to Alt-underline a character. If "...", appends it to default text. /// public string text; /// /// Alt-underlined character in menu item text. /// public char underlined; /// /// Add separator before the menu item. /// public bool separator; /// /// Checkable menu item. /// public bool checkable; /// /// Default hotkey etc. See . /// public string keys; /// /// Element where the hotkey etc (default or customized) will work. See . /// If this property applied to a class (submenu), all descendant commands without this property inherit it from the ancestor class. /// public string target; /// /// Text for . If not set, will use keys. /// public string keysText; /// /// Image string. /// The factory action receives this string in parameters. It can load image and set menu item's Icon property. /// If factory action not used or does not set Image property and does not set image=null, this class loads image from database or exe or script resources and sets Icon property. The resource file can be xaml (for example converted from svg) or png etc. If using Visual Studio, to add an image to resources set its build action = Resource. More info: . /// public string image; /// /// Tooltip text for the menu item and toolbar button (see ). /// The displayed tooltip also contains menu item text in the first line. /// public string tooltip; /// /// A string or other value to pass to the factory action. /// public object param; /// /// Don't add the MenuItem to menu. /// public bool hide; /// /// Don't change the enabled state indirectly when changing that of the parent menu. /// public bool noIndirectDisable; /// /// Sets menu item text = method/type name with spaces instead of _ , like Select_all -> "Select all". /// /// [](xref:caller_info) public CommandAttribute([CallerLineNumber] int l_ = 0) { order_ = l_; } /// /// Specifies menu item text. /// /// Menu item text. Use _ to Alt-underline a character, like "_Copy". /// [](xref:caller_info) public CommandAttribute(string text, [CallerLineNumber] int l_ = 0) { this.text = text; order_ = l_; } /// /// Specifies Alt-underlined character. Sets menu item text = method/type name with spaces instead of _ , like Select_all -> "Select all". /// /// Character to underline. /// [](xref:caller_info) public CommandAttribute(char underlined, [CallerLineNumber] int l_ = 0) { this.underlined = underlined; order_ = l_; } } ================================================ FILE: Au.Controls/KMenuCommands/KMenuCommands.cs ================================================ using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Xml.Linq; //TODO3: when a checkbox button command invoked with a hotkey, now does not change check state in menu and toolbar. // Only in Edit menu. Even if target="" and scintilla not focused. Works well in other menus. Don't know why. // Currently affected code explicitly changes check state. namespace Au.Controls; /// /// Builds a WPF window menu with submenus and items that execute static methods defined in a class and nested classes. /// Supports xaml/png/etc images, key/mouse shortcuts, auto-Alt-underline, easy creating of toolbar buttons and context menus with same/synchronized properties (command, text, image, enabled, checked, etc). /// /// /// Creates submenus from public static nested types with . /// Creates executable menu items from public static methods with . /// From each such type and method creates a object that you can access through indexer. /// Supports methods public static void Method() and public static void Method(MenuItem). /// /// /// /// /// /// public partial class KMenuCommands { readonly Dictionary _d = new(200); Menu _menubar; /// /// Builds a WPF window menu with submenus and items that execute static methods defined in a class and nested classes. /// See example in class help. /// /// A type that contains nested types with methods. Must be in single source file (not partial class). /// An empty Menu object. This function adds items to it. /// Automatically insert _ in item text for Alt-underlining where not specified explicitly. /// Optional callback function that is called for each menu item. Can create menu items, set properties, create toolbar buttons, etc. /// Duplicate name. Use . public KMenuCommands(Type commands, Menu menu, bool autoUnderline = true, Action itemFactory = null) { _menubar = menu; _CreateMenu(commands, menu, autoUnderline, itemFactory); } void _CreateMenu(Type type, ItemsControl parentMenu, bool autoUnderline, Action itemFactory, List added = null, string namePrefix_ = null, string inheritTarget_ = null) { var am = type.GetMembers(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly); if (am.Length == 0) { //dynamic submenu parentMenu.Items.Add(new Separator()); return; } List<(MemberInfo mi, CommandAttribute a)> list = new(am.Length); foreach (var mi in am) { var ca = mi.GetCustomAttribute(false); //var ca = mi.GetCustomAttributes().OfType().FirstOrDefault(); //CommandAttribute and inherited. Similar speed. Don't need because factory action receives MemberInfo an can get other attributes from it. if (ca != null) list.Add((mi, ca)); } var au = new List(); foreach (var (mi, ca) in list.OrderBy(o => o.a.order_)) { if (ca.separator && !ca.hide) parentMenu.Items.Add(new Separator()); ca.target ??= inheritTarget_; string text = ca.text, buttonText, dots = null; //menu item text, possibly with _ for Alt-underline if (text == "...") { dots = text; text = null; } if (text != null) { buttonText = StringUtil.RemoveUnderlineChar(text, '_'); } else { buttonText = text = mi.Name.Replace('_', ' ') + dots; char u = ca.underlined; if (u != default) { int i = text.IndexOf(u); if (i >= 0) text = text.Insert(i, "_"); else print.it($"Alt-underline character '{u}' not found in \"{text}\""); } } var namePrefix = ca.namePrefix ?? namePrefix_; string name = namePrefix + (ca.name ?? mi.Name); var c = new Command(this, name, text, mi, ca); _d.Add(name, c); added?.Add(name); c.ButtonText = buttonText; if (ca.tooltip is string tt) c.ButtonTooltip = parentMenu is Menu ? tt : $"{buttonText}{(ca.checkable ? " (option)" : null)}.\n{tt}"; else if (ca.checkable) c.ButtonTooltip = $"{buttonText} (option)"; FactoryParams f = null; if (itemFactory != null) { f = new FactoryParams(c, mi) { text = text, image = ca.image, param = ca.param }; itemFactory(f); if (c.MenuItem == null) c.SetMenuItem_(f.text, f.image); //did not call SetMenuItem } else { if (c.MenuItem == null) c.SetMenuItem_(text, ca.image); } if (!ca.keysText.NE()) c.MenuItem.InputGestureText = ca.keysText; if (autoUnderline && c.MenuItem.Header is string s && _FindUnderlined(s, out char uc)) au.Add(char.ToLower(uc)); if (ca.checkable) c.MenuItem.IsCheckable = true; if (c.ButtonTooltip != null) c.MenuItem.ToolTip = c.ButtonTooltip; if (ca.noIndirectDisable) c.NoIndirectDisable = true; if (parentMenu is Menu m1) c.MenuItem.MinHeight = 22; if (!ca.hide) parentMenu.Items.Add(c.MenuItem); if (mi is TypeInfo ti) { _CreateMenu(ti, c.MenuItem, autoUnderline, itemFactory, added, namePrefix, ca.target); if (ti.GetDeclaredMethod("_Init") is { } init) init.Invoke(null, [this, c]); } } if (autoUnderline) { foreach (var v in parentMenu.Items) { if (v is MenuItem m && m.Header is string s && s.Length > 0 && !_FindUnderlined(s, out _)) { int i = 0; for (; i < s.Length; i++) { char ch = s[i]; if (!char.IsLetterOrDigit(ch)) continue; ch = char.ToLower(ch); if (!au.Contains(ch)) { au.Add(ch); break; } } if (i == s.Length) i = 0; m.Header = s.Insert(i, "_"); } } } static bool _FindUnderlined(string s, out char u) { u = default; int i = 0; g1: i = s.IndexOf('_', i) + 1; if (i == 0 || i == s.Length) return false; u = s[i++]; if (u == '_') goto g1; return true; } } /// /// Gets a Command by name. /// /// Method name, for example "Select_all". Or nested type name if it's a submenu-item. /// public Command this[string command] => _d[command]; /// /// Tries to find a Command by name. Returns false if not found. /// Same as the indexer, but does not throw exception when not found. /// /// Method name, for example "Select_all". Or nested type name if it's a submenu-item. /// public bool TryFind(string command, out Command c) => _d.TryGetValue(command, out c); /// /// Adds to target's InputBindings all keys etc where CommandAttribute.target == name. /// /// /// public void BindKeysTarget(UIElement target, string name) { //print.it($"---- {name} = {target}"); foreach (var c in _d.Values) { var ca = c.Attribute; var keys = c.Keys; if (!keys.NE() && ca.target == name) { //print.it(c, keys); int i = keys.Find(", "); if (i < 0) _Add(keys); else foreach (var v in keys.Split(", ")) _Add(v); void _Add(string s) { if (!Au.keys.more.parseHotkeyString(s, out var mod, out var key, out var mouse)) { c.CustomizingError("invalid keys: " + s); return; } if (key != default) target.InputBindings.Add(new KeyBinding(c, key, mod)); else if (target is System.Windows.Interop.HwndHost) c.CustomizingError(s + ": mouse shortcuts don't work in the target control"); else target.InputBindings.Add(new MouseBinding(c, new MouseGesture(mouse, mod))); //FUTURE: support mouse shortcuts in HwndHost //if (target is System.Windows.Interop.HwndHost hh) { // hh.MessageHook += _Hh_MessageHook; // //or use native mouse hook //} else { // target.InputBindings.Add(new MouseBinding(this, new MouseGesture(mouse, mod))); //} } var mi = c.MenuItem; var s = mi.InputGestureText; if (s.NE()) s = keys; else s = s + ", " + keys; mi.InputGestureText = s; } } //let global key bindings work in any window of this thread, not only when target (main window) is active. Never mind mouse bindings. if (name == "") { var a = target.InputBindings.OfType().ToArray(); if (a.Length > 0) { EventManager.RegisterClassHandler(typeof(Window), UIElement.KeyDownEvent, new KeyEventHandler(_KeyDown)); //InputManager.Current.PreProcessInput += _App_PreProcessInput; //works too, but more events void _KeyDown(object source, KeyEventArgs e) { if (!process.IsLaMainThread_) return; //perf.first(); if (e.Handled) return; var k = e.Key; if (k == Key.System) k = e.SystemKey; if (k is Key.LeftCtrl or Key.LeftShift or Key.LeftAlt or Key.RightCtrl or Key.RightShift or Key.RightAlt or Key.LWin or Key.RWin or Key.DeadCharProcessed or Key.ImeProcessed) return; //print.it(k); ModifierKeys mod = 0; bool haveMod = false; foreach (var kb in a) { //print.it(kb.Command); if (kb.Key != k) continue; if (!haveMod) { haveMod = true; mod = Keyboard.Modifiers; } if (kb.Modifiers != mod) continue; var c = kb.Command; var cp = kb.CommandParameter; if (c.CanExecute(cp)) c.Execute(cp); e.Handled = true; break; //note: execute even if main window disabled. Maybe the command works in current window. Or maybe user wants to save (Ctrl+S). } //perf.nw(); //fast } //void _PreProcessInput(object sender, PreProcessInputEventArgs e) { // if (e.Canceled) return; // var re = e.StagingItem.Input.RoutedEvent; // if (re == Keyboard.KeyDownEvent && e.StagingItem.Input is KeyEventArgs ke) { // var k = ke.Key; if (k == Key.System) k = ke.SystemKey; // if (k is Key.LeftCtrl or Key.LeftShift or Key.LeftAlt or Key.RightCtrl or Key.RightShift or Key.RightAlt or Key.LWin or Key.RWin or Key.DeadCharProcessed or Key.ImeProcessed) return; // print.it(k); // //} else { //no mouse events in hwndhosted control. It's ok, don't need global mouse shortcuts. Normal WPF bindings don't work too. // // print.it(re); // } //} } } } public string DefaultFile { get; private set; } public string UserFile { get; private set; } /// /// Adds toolbar buttons specified in xmlFileCustomized and xmlFileDefault. /// Applies customizations specified there. /// /// XML file containing default toolbar buttons. See Default\Commands.xml in editor project. /// XML file containing user-modified commands and toolbar buttons. Can be null. The file can exist or not. /// Empty toolbars where to add buttons. XML tag = Name property. public void InitToolbarsAndCustomize(string xmlFileDefault, string xmlFileCustomized, ToolBar[] toolbars) { DefaultFile = xmlFileDefault; UserFile = xmlFileCustomized; var a = LoadFiles(); if (a == null) return; foreach (var x in a) { ToolBar tb = null; var tbname = x.Name.LocalName; if (tbname != "menu") { tb = toolbars.FirstOrDefault(o => o.Name == tbname); if (tb == null) { Debug_.Print("Unknown toolbar " + tbname); continue; } } foreach (var v in x.Elements()) { if (_d.TryGetValue(v.Name.LocalName, out var c)) c.Customize_(v, tb); } } } /// /// Loads and merges default and customized commands files. /// public XElement[] LoadFiles() { static XElement[] _LoadFile(string file) { try { return XmlUtil.LoadElem(file).Elements().ToArray(); } catch (Exception ex) { print.it($"<>Failed to load file {file}<>. <_>{ex.ToStringWithoutStack()}"); return null; } } var a = _LoadFile(DefaultFile); if (a == null) return null; var ac = UserFile != null && filesystem.exists(UserFile, true).File ? _LoadFile(UserFile) : null; if (ac != null) { //replace a elements with elements that exist in ac. If some toolbar does not exist there, use default. for (int i = 0; i < a.Length; i++) { var name = a[i].Name; foreach (var x in ac) if (x.Name == name && x.HasElements) { _AddMissingButtons(a[i], x); a[i] = x; break; } } void _AddMissingButtons(XElement xDef, XElement xUser) { if (xUser.Name.LocalName == "menu") return; foreach (var v in xDef.Elements().Except(xUser.Elements(), s_xmlNameComparer)) { //rejected: hide new buttons to avoid pushing some old buttons to the overflow if the toolbar size cannot grow. // Then new and probably important features would be used rarely and/or inconveniently. // Instead hide just duplicates, eg when the new button actually is an old button moved to this toolbar. //v.SetAttributeValue("hide", "always"); foreach (var tb in ac) if (tb != xUser && tb.Elements(v.Name).Any()) { v.SetAttributeValue("hide", "always"); break; } if (v.PreviousNode is XElement xdPrev && xUser.Element(xdPrev.Name) is { } xuPrev) xuPrev.AddAfterSelf(v); //insert in default place else xUser.Add(v); //add to the end } } } return a; } //currently not used. //public void AddExtensionMenus(Type commands, MenuItem parentMenu = null, bool autoUnderline = true) { // ItemsControl pa = parentMenu ?? _menubar as ItemsControl; // _extensions ??= new(); // if (_extensions.Remove(commands.Name, out var t)) { // foreach (var v in t.menus) pa.Items.Remove(v); // foreach (var v in t.commands) _d.Remove(v); // } // int n = pa.Items.Count; // List added = new(); // _CreateMenu(commands, pa, autoUnderline, null, added); // _extensions.Add(commands.Name, (pa.Items.OfType().Skip(n).ToArray(), added)); //} //Dictionary commands)> _extensions; ////_extensions used to support updating the same extension. Remove the code if don't need. } ================================================ FILE: Au.Controls/KPanels/FlexStackPanel.cs ================================================ // Copyright (c) 2013 xmetropol. // This code is distributed under the Microsoft Public License (Ms-PL). // All rights reserved. //https://www.codeproject.com/Articles/598123/WPF-Flexible-StackPanel using System.Windows; using System.Windows.Controls; using System.Windows.Media; namespace Au.Controls; public enum FlexStretchDirection { DownOnly, UpOnly, Both, None, } public class FlexStackPanel : Panel { #region Static Fields private const double Tolerance = 0.001; public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register ("Orientation", typeof(Orientation), typeof(FlexStackPanel), new FrameworkPropertyMetadata(Orientation.Horizontal, FrameworkPropertyMetadataOptions.AffectsMeasure, OnOrientationChanged)); public static readonly DependencyProperty StretchDirectionProperty = DependencyProperty.Register ("StretchDirection", typeof(FlexStretchDirection), typeof(FlexStackPanel), new FrameworkPropertyMetadata(default(FlexStretchDirection), FrameworkPropertyMetadataOptions.AffectsMeasure)); public static readonly DependencyPropertyKey HasOverflowedChildrenPropertyKey = DependencyProperty.RegisterReadOnly ("HasOverflowedChildren", typeof(bool), typeof(FlexStackPanel), new PropertyMetadata(default(bool))); public static readonly DependencyProperty HasOverflowedChildrenProperty = HasOverflowedChildrenPropertyKey.DependencyProperty; public static readonly DependencyProperty MinSlotSizeProperty = DependencyProperty.RegisterAttached ("MinSlotSize", typeof(double?), typeof(FlexStackPanel), new FrameworkPropertyMetadata(default(double?), FrameworkPropertyMetadataOptions.AffectsParentMeasure)); public static readonly DependencyProperty MaxSlotSizeProperty = DependencyProperty.RegisterAttached ("MaxSlotSize", typeof(double?), typeof(FlexStackPanel), new FrameworkPropertyMetadata(default(double?), FrameworkPropertyMetadataOptions.AffectsParentMeasure)); private static readonly DependencyPropertyKey IsOverflowedPropertyKey = DependencyProperty.RegisterAttachedReadOnly ("IsOverflowed", typeof(bool), typeof(FlexStackPanel), new PropertyMetadata(default(bool))); public static readonly DependencyProperty IsOverflowedProperty = IsOverflowedPropertyKey.DependencyProperty; public static readonly DependencyProperty MeasureToSlotProperty = DependencyProperty.Register ("MeasureToSlot", typeof(bool), typeof(FlexStackPanel), new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.AffectsMeasure)); public static readonly DependencyProperty ShrinkOnOverflowProperty = DependencyProperty.RegisterAttached ("ShrinkOnOverflow", typeof(bool), typeof(FlexStackPanel), new FrameworkPropertyMetadata(default(bool), FrameworkPropertyMetadataOptions.AffectsMeasure, OnAffectMeasurePropertyChanged)); public static readonly DependencyProperty IgnoreMinConstraintsProperty = DependencyProperty.Register ("IgnoreMinConstraints", typeof(bool), typeof(FlexStackPanel), new FrameworkPropertyMetadata(default(bool), FrameworkPropertyMetadataOptions.AffectsMeasure)); public static readonly DependencyProperty IgnoreMaxConstraintsProperty = DependencyProperty.Register ("IgnoreMaxConstraints", typeof(bool), typeof(FlexStackPanel), new FrameworkPropertyMetadata(default(bool), FrameworkPropertyMetadataOptions.AffectsMeasure)); #endregion #region Fields private readonly Dictionary childToConstraint = new Dictionary(); private bool isMeasureDirty; private bool isMeasureOverrideInProgress; private bool isHorizontal = true; private List orderedSequence; private Slot[] slots; #endregion #region Ctors static FlexStackPanel() { DefaultStyleKeyProperty.OverrideMetadata (typeof(FlexStackPanel), new FrameworkPropertyMetadata(typeof(FlexStackPanel))); } #endregion #region Properties protected override bool HasLogicalOrientation { get { return true; } } public bool HasOverflowedChildren { get { return (bool)GetValue(HasOverflowedChildrenProperty); } private set { SetValue(HasOverflowedChildrenPropertyKey, value); } } public bool IgnoreMaxConstraints { get { return (bool)GetValue(IgnoreMaxConstraintsProperty); } set { SetValue(IgnoreMaxConstraintsProperty, value); } } public bool IgnoreMinConstraints { get { return (bool)GetValue(IgnoreMinConstraintsProperty); } set { SetValue(IgnoreMinConstraintsProperty, value); } } protected override Orientation LogicalOrientation { get { return Orientation; } } public bool MeasureToSlot { get { return (bool)GetValue(MeasureToSlotProperty); } set { SetValue(MeasureToSlotProperty, value); } } public Orientation Orientation { get { return (Orientation)GetValue(OrientationProperty); } set { SetValue(OrientationProperty, value); } } public FlexStretchDirection StretchDirection { get { return (FlexStretchDirection)GetValue(StretchDirectionProperty); } set { SetValue(StretchDirectionProperty, value); } } #endregion #region Methods public static bool GetIsOverflowed(UIElement element) { return (bool)element.GetValue(IsOverflowedProperty); } public static double? GetMaxSlotSize(UIElement element) { return (double?)element.GetValue(MaxSlotSizeProperty); } public static double? GetMinSlotSize(UIElement element) { return (double?)element.GetValue(MinSlotSizeProperty); } public static bool GetShrinkOnOverflow(UIElement element) { return (bool)element.GetValue(ShrinkOnOverflowProperty); } public static void SetMaxSlotSize(UIElement element, double? value) { element.SetValue(MaxSlotSizeProperty, value); } public static void SetMinSlotSize(UIElement element, double? value) { element.SetValue(MinSlotSizeProperty, value); } public static void SetShrinkOnOverflow(UIElement element, bool value) { element.SetValue(ShrinkOnOverflowProperty, value); } protected override Size ArrangeOverride(Size finalSize) { var size = new Size(isHorizontal ? 0 : finalSize.Width, !isHorizontal ? 0 : finalSize.Height); var childrenCount = Children.Count; var rc = new Rect(); for (var index = 0; index < childrenCount; index++) { var child = orderedSequence[index]; if (GetIsOverflowed(child)) { child.Arrange(new Rect()); continue; } var slotVal = slots[index].Val; if (isHorizontal) { rc.Width = double.IsInfinity(slotVal) ? child.DesiredSize.Width : slotVal; rc.Height = Math.Max(finalSize.Height, child.DesiredSize.Height); size.Width += rc.Width; size.Height = Math.Max(size.Height, rc.Height); child.Arrange(rc); rc.X += rc.Width; } else { rc.Width = Math.Max(finalSize.Width, child.DesiredSize.Width); rc.Height = double.IsInfinity(slotVal) ? child.DesiredSize.Height : slotVal; size.Width = Math.Max(size.Width, rc.Width); size.Height += rc.Height; child.Arrange(rc); rc.Y += rc.Height; } } return new Size(Math.Max(finalSize.Width, size.Width), Math.Max(finalSize.Height, size.Height)); } protected override Size MeasureOverride(Size availableSize) { try { isMeasureOverrideInProgress = true; var ignoreMinConstraints = IgnoreMinConstraints; var ignoreMaxConstraints = IgnoreMaxConstraints; for (var i = 0; i < 3; i++) { isMeasureDirty = false; var childrenDesiredSize = new Size(); var childrenCount = Children.Count; if (childrenCount == 0) return childrenDesiredSize; var childConstraint = GetChildrenConstraint(availableSize); var uniSize = GetUniformSize(availableSize); slots = new Slot[childrenCount]; orderedSequence = Children.Cast().ToList(); for (var index = 0; index < childrenCount; index++) { if (isMeasureDirty) break; var child = orderedSequence[index]; var minLength = (ignoreMinConstraints ? (double?)0.0 : null) ?? GetMinSlotSize(child); var maxLength = (ignoreMaxConstraints ? (double?)0.0 : null) ?? GetMaxSlotSize(child); var frameworkChild = child as FrameworkElement; if (frameworkChild != null) { var margin = frameworkChild.Margin; minLength = minLength ?? (isHorizontal ? frameworkChild.MinWidth : frameworkChild.MinHeight); maxLength = maxLength ?? (isHorizontal ? frameworkChild.MaxWidth + margin.Width() : frameworkChild.MaxHeight + margin.Height()); } minLength = minLength ?? 0.0; maxLength = maxLength ?? double.PositiveInfinity; MeasureChild(child, childConstraint); if (isHorizontal) { childrenDesiredSize.Width += child.DesiredSize.Width; slots[index] = new Slot(minLength.Value, maxLength.Value, StretchDirection == FlexStretchDirection.Both ? uniSize.Width : child.DesiredSize.Width); childrenDesiredSize.Height = Math.Max(childrenDesiredSize.Height, child.DesiredSize.Height); } else { childrenDesiredSize.Height += child.DesiredSize.Height; slots[index] = new Slot(minLength.Value, maxLength.Value, StretchDirection == FlexStretchDirection.Both ? uniSize.Height : child.DesiredSize.Height); childrenDesiredSize.Width = Math.Max(childrenDesiredSize.Width, child.DesiredSize.Width); } } if (isMeasureDirty) continue; var current = slots.Sum(s => s.Val); var target = GetSizePart(availableSize); var finalSize = new Size (Math.Min(availableSize.Width, isHorizontal ? current : childrenDesiredSize.Width), Math.Min(availableSize.Height, isHorizontal ? childrenDesiredSize.Height : current)); if (double.IsInfinity(target)) return finalSize; RecalcSlots(current, target); // Current length is greater than available and we have no possibility to stretch down -> mark elements as overflow current = 0.0; for (var index = 0; index < childrenCount; index++) { var child = orderedSequence[index]; var slot = slots[index]; if (GetShrinkOnOverflow(child) && IsGreater(current + slot.Val, target, Tolerance) && IsGreater(target, current, Tolerance)) { var rest = IsGreater(target, current, Tolerance) ? target - current : 0.0; if (IsGreater(rest, slot.Min, Tolerance)) slot.Val = rest; } current += slot.Val; SetIsOverflowed(child, IsGreater(current, target, Tolerance)); } HasOverflowedChildren = current > target; if (MeasureToSlot) RemeasureChildren(finalSize); finalSize = new Size (Math.Min(availableSize.Width, isHorizontal ? target : childrenDesiredSize.Width), Math.Min(availableSize.Height, isHorizontal ? childrenDesiredSize.Height : target)); if (isMeasureDirty) continue; return finalSize; } } finally { isMeasureOverrideInProgress = false; } return new Size(); } protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved) { base.OnVisualChildrenChanged(visualAdded, visualRemoved); var removedUIElement = visualRemoved as UIElement; if (removedUIElement != null) childToConstraint.Remove(removedUIElement); } private static void ExpandSlots(IEnumerable slots, double target) { var sortedSlots = slots.OrderBy(v => v.Val).ToList(); var maxValidTarget = sortedSlots.Sum(s => s.Max); if (maxValidTarget < target) { foreach (var slot in sortedSlots) slot.Val = slot.Max; return; } do { var tmpTarget = target; for (var iSlot = sortedSlots.Count - 1; iSlot >= 0; iSlot--) { var slot = sortedSlots[iSlot]; if (slot.Val * (iSlot + 1) <= tmpTarget) { var avg = tmpTarget / (iSlot + 1); var success = true; for (var jSlot = iSlot; jSlot >= 0; jSlot--) { var tslot = sortedSlots[jSlot]; tslot.Val = Math.Min(tslot.Max, avg); // Max constraint skip success expand on this iteration if (Math.Abs(avg - tslot.Val) <= Tolerance) continue; target -= tslot.Val; success = false; sortedSlots.RemoveAt(jSlot); } if (success) return; break; } tmpTarget -= slot.Val; } } while (sortedSlots.Count > 0); } private Size GetChildrenConstraint(Size availableSize) { return new Size (isHorizontal ? double.PositiveInfinity : availableSize.Width, !isHorizontal ? double.PositiveInfinity : availableSize.Height); } private double GetSizePart(Size size) { return isHorizontal ? size.Width : size.Height; } private Size GetUniformSize(Size availableSize) { var childrenCount = Children.Count; if (childrenCount == 0) return new Size(); return new Size (isHorizontal ? availableSize.Width / childrenCount : availableSize.Width, !isHorizontal ? availableSize.Height / childrenCount : availableSize.Height); } private static bool IsGreater(double a, double b, double tolerance) { return a - b > tolerance; } private void MeasureChild(UIElement child, Size childConstraint) { Size lastConstraint; if ((child.IsMeasureValid && childToConstraint.TryGetValue(child, out lastConstraint) && lastConstraint.Equals(childConstraint))) return; child.Measure(childConstraint); childToConstraint[child] = childConstraint; } private void OnAffectMeasureChanged() { if (isMeasureOverrideInProgress) isMeasureDirty = true; } private static void OnAffectMeasurePropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs) { var flexStackPanel = VisualTreeHelper.GetParent(dependencyObject) as FlexStackPanel; if (flexStackPanel != null) flexStackPanel.OnAffectMeasureChanged(); } private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var panel = (FlexStackPanel)d; panel.isHorizontal = panel.Orientation == Orientation.Horizontal; } private void RecalcSlots(double current, double target) { var shouldShrink = IsGreater(current, target, Tolerance) && (StretchDirection == FlexStretchDirection.DownOnly || StretchDirection == FlexStretchDirection.Both); var shouldExpand = IsGreater(target, current, Tolerance) && (StretchDirection == FlexStretchDirection.UpOnly || StretchDirection == FlexStretchDirection.Both); if (shouldShrink) ShrinkSlots(slots, target); else if (shouldExpand) ExpandSlots(slots, target); } private void RemeasureChildren(Size availableSize) { var childrenCount = Children.Count; if (childrenCount == 0) return; var childConstraint = GetChildrenConstraint(availableSize); for (var index = 0; index < childrenCount; index++) { var child = orderedSequence[index]; if (Math.Abs(GetSizePart(child.DesiredSize) - slots[index].Val) > Tolerance) MeasureChild(child, new Size(isHorizontal ? slots[index].Val : childConstraint.Width, !isHorizontal ? slots[index].Val : childConstraint.Height)); } } private static void SetIsOverflowed(UIElement element, bool value) { element.SetValue(IsOverflowedPropertyKey, value); } private static void ShrinkSlots(IEnumerable slots, double target) { var sortedSlots = slots.OrderBy(v => v.Val).ToList(); var minValidTarget = sortedSlots.Sum(s => s.Min); if (minValidTarget > target) { foreach (var slot in sortedSlots) slot.Val = slot.Min; return; } do { var tmpTarget = target; for (var iSlot = 0; iSlot < sortedSlots.Count; iSlot++) { var slot = sortedSlots[iSlot]; if (slot.Val * (sortedSlots.Count - iSlot) >= tmpTarget) { var avg = tmpTarget / (sortedSlots.Count - iSlot); var success = true; for (var jSlot = iSlot; jSlot < sortedSlots.Count; jSlot++) { var tslot = sortedSlots[jSlot]; tslot.Val = Math.Max(tslot.Min, avg); // Min constraint skip success expand on this iteration if (Math.Abs(avg - tslot.Val) <= Tolerance) continue; target -= tslot.Val; success = false; sortedSlots.RemoveAt(jSlot); jSlot--; } if (success) return; break; } tmpTarget -= slot.Val; } } while (sortedSlots.Count > 0); } #endregion #region Nested type: Slot private class Slot { #region Fields public readonly double Max; public readonly double Min; public double Val; #endregion #region Ctors public Slot(double min, double max, double val) { Min = min; Max = max; Val = val; Val = Math.Max(min, val); Val = Math.Min(max, Val); } #endregion } #endregion } file static class MeasureUtil { public static double Height(this Thickness thickness) { return thickness.Top + thickness.Bottom; } public static double Width(this Thickness thickness) { return thickness.Left + thickness.Right; } } ================================================ FILE: Au.Controls/KPanels/ILeaf.cs ================================================ using System.Windows; using System.Windows.Controls; namespace Au.Controls; public partial class KPanels { /// /// Interface for a leaf item (panel, toolbar or document). /// public interface ILeaf { /// /// Gets or sets content of panel/toolbar/document. /// FrameworkElement Content { get; set; } /// /// true if visible, either floating or docked. /// The get function returns true even if inactive tab item. The set function makes tab item active. /// bool Visible { get; set; } /// /// true if floating and visible. /// false if docked or hidden. /// bool Floating { get; set; } /// /// If not null, clicking on the parent tab header does not change the current focus (if possible). /// The action is called if the currently focused element is in the tab control; it must set focus to some visible element or call Keyboard.ClearFocus. /// Action DontFocusTab { get; set; } Func DontActivateFloating { get; set; } /// /// Adds new leaf item (panel, toolbar or document) before or after this. /// /// /// /// /// Add "Close M-click" item in context menu. It will fire event and call if not canceled. /// Save layout etc in file. /// Interface of the new item. /// type is not Panel/Toolbar/Document, or name is null, or name panel already exists. /// /// Added items can be deleted with . /// Add documents only by the document placeholder or by added documents. Don't add other nodes by documents. /// ILeaf AddSibling(bool after, LeafType type, string name, bool canClose, bool isExtension); /// /// Deletes this leaf item added with . /// /// Added not with AddSibling. void Delete(); /// /// Renames this document. /// void Rename(string name); /// /// Gets parent elements and index. /// ParentInfo Parent { get; } /// /// After hiding or showing this leaf item. /// event EventHandler VisibleChanged; /// /// After made floating or non-floating this leaf item. /// event EventHandler FloatingChanged; /// /// When user tries to close this leaf item. /// Only if added with with canClose true. /// event System.ComponentModel.CancelEventHandler Closing; ///// ///// When opening context menu of this leaf item. ///// You can add menu items. All default items are already added. ///// //event EventHandler ContextMenuOpening; //FUTURE: reenable this if useful when ContextMenu_ will be public /// /// When this tab item selected (becomes the active item). /// event EventHandler TabSelected; /// /// When moved to other tab or stack. /// event EventHandler ParentChanged; } /// Leaf item type. public enum LeafType { None, Panel, Toolbar, Document } public struct ParentInfo { readonly DockPanel _panel; readonly FrameworkElement _elem; readonly int _index; internal ParentInfo(DockPanel panel, FrameworkElement elem, int index) { _panel = panel; _elem = elem; _index = index; } /// /// Gets DockPanel that contains or will contain . /// The first child is header, and is TextBlock or Rectangle. The second child is Content (if set) or none. /// public DockPanel Panel => _panel; /// /// Gets parent Grid if in stack, else null. /// public Grid Grid => _elem as Grid; /// /// Gets parent TabControl if in tab, else null. /// public TabControl TabControl => _elem as TabControl; /// /// Gets parent TabItem if in tab, else null. /// Its Tag is this ILeaf. /// public TabItem TabItem => TabControl?.Items[_index] as TabItem; /// /// Gets node index in parent node. If in tab, it is also tab item index. /// public int Index => _index; } } ================================================ FILE: Au.Controls/KPanels/KPanels.cs ================================================ using System.Xml.Linq; using System.Xml; using System.Windows; using System.Windows.Controls; using System.Windows.Media; namespace Au.Controls; /// /// Creates and manages window layout like in Visual Studio. /// Multiple docked movable/sizable/tabable/floatable/hidable/savable panels/toolbars/documents with splitters. /// /// /// Layout is defined in default XML file, then saved in other XML file. See Layout.xml in Au.Editor project. /// If new app version adds/removes/renames panels etc, this class automatically updates the saved layout. /// /// Let your window's ctor: /// - call ; /// - set content of all leaf items (panels, toolbars, document placeholder) like _panels["Files"].Content = new TreeView();; /// - set like _panels.Container = g => this.Content = g;. The action is called immediately and also may be called later if need to create new root element when moving a panel etc. /// If want to save user-customized layout, call when closing window or at any time before it. /// public partial class KPanels { readonly Dictionary _dictLeaf = new(); readonly Dictionary _dictUserDoc = new(); _Node _rootStack; string _xmlFile; bool _loaded; /// /// Loads layout from XML file. /// /// /// XML file containing default layout. See Default\Layout.xml in editor project. /// If starts with '<', loads from XML string instead. /// /// XML file containing user-modified layout. It will be created or updated by . If null, will not save. public void Load(string xmlFileDefault, string xmlFileCustomized) { if (_loaded) throw new InvalidOperationException(); _loaded = true; _xmlFile = xmlFileCustomized; //At first try to load xmlFileCustomized. If it does not exist or is invalid, load xmlFileDefault. string xmlFile = xmlFileCustomized; bool useDefaultXML = xmlFileCustomized == null; gRetry: if (useDefaultXML) xmlFile = xmlFileDefault; else if (useDefaultXML = !filesystem.exists(xmlFile).File) goto gRetry; try { var x = XmlUtil.LoadElem(xmlFile); if (!useDefaultXML) _AutoUpdateXml(x, xmlFileDefault); new _Node(this, x); //creates all and sets _rootStack } catch (Exception e) when (!Debugger.IsAttached) { var sErr = $"Failed to load file '{xmlFile}'.\r\n\t{e.ToStringWithoutStack()}"; if (useDefaultXML) { _xmlFile = null; dialog.showError("Cannot load window layout from XML file.", $"{sErr}\r\n\r\nReinstall the application.", expandedText: e.StackTrace); Environment.Exit(1); } else { print.warning(sErr, -1); } _dictLeaf.Clear(); _rootStack = null; useDefaultXML = true; goto gRetry; } } /// /// Saves layout to XML file xmlFileCustomized specified when calling . /// Can be called at any time. When closing window, should be called in OnClosing override after calling base.OnClosing. /// Fast. Saves only if changed. Can be called eg every 30 s. /// Does nothing if xmlFileCustomized was null. /// public void Save() { if (_xmlFile == null) return; try { s_xws ??= new XmlWriterSettings() { OmitXmlDeclaration = true, Indent = true, IndentChars = "\t" }; var b = new StringBuilder(2000); using (var x = XmlWriter.Create(b, s_xws)) { //not `using var...;` x.WriteStartDocument(); _rootStack.Save(x); } var s1 = b.ToString(); try { _savedXML ??= filesystem.loadText(_xmlFile); } catch { } if (s1 != _savedXML) { filesystem.saveText(_xmlFile, s1); _savedXML = s1; } } catch { } } static XmlWriterSettings s_xws; string _savedXML; void _AutoUpdateXml(XElement rootStack, string xmlFileDefault) { var defRootStack = XmlUtil.LoadElem(xmlFileDefault); var eOld = rootStack.Descendants("panel").Concat(rootStack.Descendants("toolbar"))/*.Concat(rootStack.Descendants("document"))*/; //same speed as with .Where(cached delegate) var eNew = defRootStack.Descendants("panel").Concat(defRootStack.Descendants("toolbar"))/*.Concat(defRootStack.Descendants("document"))*/; var cmp = new _XmlNameAttrComparer(); var added = eNew.Except(eOld, cmp); var removed = eOld.Except(eNew, cmp); if (removed.Any()) { foreach (var x in removed.ToArray()) { if (x.HasAttr("ext")) continue; _Remove(x); print.it($"Info: {x.Name} {x.Attr("name")} has been removed in this app version."); } //removes x and ancestor stacks/tabs that would then have <= 1 child static void _Remove(XElement x) { var pa = x.Parent; x.Remove(); var a = pa.Elements(); int n = a.Count(); if (n > 1) return; if (n == 1) { var f = a.First(); f.SetAttributeValue("z", pa.Attr("z")); f.SetAttributeValue("s", pa.Attr("s")); f.Remove(); pa.AddAfterSelf(f); } _Remove(pa); } } if (added.Any()) { foreach (var x in added) { //try to add near the default place string sAppend = null; //print.qm2.use=true; //print.clear(); var byDef = x.Parent.Elements().LastOrDefault(o => o != x && o.Name.LocalName is not ("stack" or "tab")); if (byDef != null && rootStack.Desc(byDef.Name, "name", byDef.Attribute("name").Value) is { } by) { bool inTab = x.Parent.Name == "tab"; if (inTab && by.Parent.Name != "tab") { var tab = new XElement("tab", x); if (by.Attribute("z") is { } a1) { a1.Remove(); tab.SetAttributeValue("z", a1.Value); } if (by.Attribute("headerAt") is { } a2) tab.SetAttributeValue("headerAt", a2.Value); by.AddAfterSelf(tab); by.Remove(); tab.AddFirst(by); } else { if (x.Parent.Name == "stack" && byDef.Parent.Name == "stack" && x.Parent.Attr("o") != by.Parent.Attr("o") && x.Parent.Elements().Count() == 2) { by.AddAfterSelf(x.Parent); by.Remove(); } else { if (!inTab) x.SetAttributeValue("z", 50); else x.SetAttributeValue("z", null); by.AddAfterSelf(x); } } string s1 = by.Name.LocalName; if (s1 == "document") s1 = "panel"; sAppend = $"next to the {by.Attr("name")} {s1}. You can move it to a better place (right-click...){(inTab ? "" : " or/and resize")}."; } else { x.SetAttributeValue("z", 50); //note: set even if was no "z", because maybe was in a tab rootStack.Add(x); sAppend = "at the bottom of the window. Right-click its header and move it to a better place."; } string sAppend2 = x.Attr(out int state, "state") && 0 != (state & 1) ? "\r\n\tNow it's hidden. To show, right-click any panel header." : null; print.it($"<>New {x.Name} in this app version: {x.Attr("name")}<>.\r\n\tIt's {sAppend}{sAppend2}"); } } //print.it(rootStack); } ///// ///// Deletes the user's saved layout file. Then next time will use the default file. ///// //public void DeleteSavedFile() { // filesystem.delete(_xmlFile); //} /// /// Action that adds the root node (Grid) to a container (for example Window), like _panels.Container = g => this.Content = g;. /// The action is called immediately and also may be called later if need to create new root element when moving a panel etc. /// public Action Container { get => _setContainer; set { _setContainer = value; var e = _rootStack.Elem as Grid; if (e.Parent == null) _setContainer(e); } } Action _setContainer; //public Grid RootElem => _rootStack.Elem as Grid; /// /// Gets interface of a leaf item (panel, toolbar or document). /// /// null if not found. /// /// It is a document (not panel/toolbar) added with . User documents are in a separate dictionary, to avoid name conflicts. public ILeaf this[string name, bool userDocument = false] { get { var d = userDocument ? _dictUserDoc : _dictLeaf; d.TryGetValue(name, out var v); return v; } } /// /// Gets interface of container leaf item (panel, toolbar or document). /// /// Leaf's Content or any descendant. /// public ILeaf this[DependencyObject e] { get { while (e != null) { if (e is _DockPanelWithBorder d && d.Tag is ILeaf f) return f; e = VisualTreeHelper.GetParent(e); //same with LogicalTreeHelper } throw new NotFoundException(); } } public ILeaf AddNewExtension(bool toolbar, string name, ILeaf where = null, bool after = false) { if (name.NE()) throw new ArgumentException(); if (where == null) { where = _rootStack.LastChild as ILeaf; after = true; print.it($"Info: added new {(toolbar ? "toolbar" : "panel")} {name}. It is at the bottom of the window. Right-click its header and move it to a better place."); } return where.AddSibling(after, toolbar ? LeafType.Toolbar : LeafType.Panel, name, canClose: false, isExtension: true); } //rejected. Rarely used. Can set in Container action. ///// ///// Background brush of the root grid. ///// Set before . ///// //public Brush Background { // get => _gridBackground; // set => _gridBackground = value; //} /// /// Background brush of panel/document header and tab strip. Default Brushes.LightSteelBlue. /// Set before . /// public Brush HeaderBrush { get; set; } = Brushes.LightSteelBlue; //rejected. Looks ugly when different color, unless white. ///// ///// Background brush of tab strip. Default Brushes.LightSteelBlue. ///// Set before . ///// //public Brush TabBrush { get; set; } = Brushes.LightSteelBlue; /// /// Background brush of splitters. /// Set before . /// public Brush SplitterBrush { get; set; } /// /// Border color of panels and documents. /// Set before . /// If not set, no borders will be added. /// public Brush BorderBrush { get; set; } /// /// Gets top-level window, for example to use as owner of menus/dialogs. /// Note: it may not be direct container of the root element. /// Window _ContainerWindow => _window ??= Window.GetWindow(_rootStack.Elem); Window _window; class _XmlNameAttrComparer : IEqualityComparer { public bool Equals(XElement x, XElement y) => x.Attr("name") == y.Attr("name"); public int GetHashCode(XElement obj) => obj.Attr("name").GetHashCode(); //fast, same as with XName _name. } } ================================================ FILE: Au.Controls/KPanels/_Floating.cs ================================================ using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Media; using System.Windows.Interop; using System.Windows.Input; namespace Au.Controls; public partial class KPanels { partial class _Node { class _Floating : Window { readonly _Node _node; readonly Window _owner; readonly bool _noActivate; public _Floating(_Node node, bool onDrag) { _node = node; _owner = _node._pm._ContainerWindow; _noActivate = _node._IsToolbarsNode || _node.DontActivateFloating != null; //workaround for: if a HwndHost-ed native control in this panel is focused, WPF may activate the floating panel and disable next mouse click if (Keyboard.FocusedElement == null) { var wFocus = Api.GetFocus(); if (!wFocus.Is0 && wFocus.IsChildOf(_owner.Hwnd())) { if (null != _node.Elem.FindVisualDescendant(o => o is HwndHost h && h.Handle == wFocus.Handle)) _owner.Focus(); } } var style = WS.THICKFRAME | WS.POPUP | WS.CLIPCHILDREN; if (_node._IsStack) style |= WS.CAPTION; bool unowned = _node._windowStyle.Has(_WindowStyle.Unowned); if (unowned) style |= WS.CAPTION | WS.MAXIMIZEBOX | WS.MINIMIZEBOX | WS.SYSMENU; var estyle = unowned ? WSE.APPWINDOW | WSE.WINDOWEDGE : /*WSE.TOOLWINDOW |*/ WSE.WINDOWEDGE; //note: don't use WS_EX_TOOLWINDOW, because of Win11 bug: no WM_DPICHANGED when screen DPI changes; tested workarounds don't work well. RECT rect = default; bool defaultRect = onDrag | (_node._floatSavedRect == null); if (defaultRect) { var c2 = _node._ParentIsTab ? _node.Parent._elem : _node._elem; if (c2.IsVisible) { rect = c2.RectInScreen(); Dpi.AdjustWindowRectEx(c2, ref rect, style, estyle); } else { var p = mouse.xy; rect = (p.x - 10, p.y - 10, 200, 200); } } base.SourceInitialized += (_, _) => { var w = this.Hwnd(); w.SetStyle(style); if (_noActivate) w.SetExStyle(WSE.NOACTIVATE, WSFlags.Add); }; base.Title = script.name + " - " + _node.ToString(); base.WindowStartupLocation = WindowStartupLocation.Manual; base.ShowActivated = false; if (!unowned) { if (!_node._windowStyle.Has(_WindowStyle.Topmost)) base.Owner = _owner; /*base.WindowStyle = WindowStyle.ToolWindow;*/ base.ShowInTaskbar = false; //never mind: if false, WPF creates a "Hidden Window", probably as owner, even if owner specified } if (_node._windowStyle.Has(_WindowStyle.Topmost)) base.Topmost = true; if (defaultRect) this.SetRect(rect); else WndSavedRect.Restore(this, _node._floatSavedRect); _owner.Closing += _Owner_Closing; _owner.IsVisibleChanged += _Owner_IsVisibleChanged; } public void ShowIfOwnerVisible() { if (_owner.IsVisible) Show(); } private void _Owner_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) { //never mind: no event if closed from outside. bool visible = (bool)e.NewValue; //print.it("owner visible", visible); if (visible) Show(); else Hide(); } private void _Owner_Closing(object sender, CancelEventArgs e) { //print.it("owner closing", e.Cancel); if (!e.Cancel) this.Close(); } protected override void OnClosing(CancelEventArgs e) { //print.it("closing", e.Cancel, _node._state); if (!e.Cancel) { _owner.IsVisibleChanged -= _Owner_IsVisibleChanged; Save(); Content = null; _node._floatWindow = null; } base.OnClosing(e); } public void Save() { //print.it("save"); _node._floatSavedRect = new WndSavedRect(this).ToString(); } protected override void OnSourceInitialized(EventArgs e) { base.OnSourceInitialized(e); var hs = PresentationSource.FromVisual(this) as HwndSource; hs.AddHook(WndProc); } private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { var w = (wnd)hwnd; //WndUtil.PrintMsg(w, msg, wParam, lParam); switch (msg) { case Api.WM_MOUSEACTIVATE: bool no = Math2.LoWord(lParam) != Api.HTCLIENT; if (!no) { if (_noActivate) { //activate if clicked a focusable element var ie = Mouse.DirectlyOver; if (ie == null) { //native child control? if (_node.DontActivateFloating != null) { var c = wnd.fromMouse(WXYFlags.Raw); if (c != w && !c.Is0) { if (wnd.Internal_.ToWpfElement(c) is { } e) no = _node.DontActivateFloating(e); } } } else { no = true; for (var e = ie as UIElement; e != null && e != _node._elem; e = VisualTreeHelper.GetParent(e) as UIElement) { //print.it(e, e.Focusable); if (e.Focusable) { if (e is ButtonBase) { //e.Focusable = false; //Dispatcher.InvokeAsync(() => e.Focusable = true); } else { no = _node.DontActivateFloating?.Invoke(e) == true; } break; } } } } else if (!_node._IsStack) { //activate if clicked not panel header and not tab header FrameworkElement e; if (_node._IsTab) e = (_node._tab.tc.SelectedItem as TabItem)?.Content as FrameworkElement; else e = _node._leaf.content; no = e != null && !e.RectInScreen().Contains(mouse.xy); } } if (no) { //print.it("MA_NOACTIVATE"); w.ZorderTop(); handled = true; return (IntPtr)Api.MA_NOACTIVATE; //never mind: if clicked or dragged resizable border, on mouse up OS activates the window. } break; case Api.WM_CLOSE: if (w.IsMinimized) w.ShowNotMinimized(); //without this would be exception (why?) if (_node._state == _DockState.Float) { //closing not by _SetDockState handled = true; _node._SetDockState(0); } break; } return default; } //Currently supports only moving but not docking. Docking is implemented in _ContextMenu_Move+_MoveTo. public void Drag(POINT p) { //bool canDock = false; //_DockTarget target = null; var w = this.Hwnd(); RECT r = w.Rect; POINT offs = (p.x - r.left, p.y - r.top); bool ok = WndUtil.DragLoop(w, MButtons.Left, d => { if (d.msg.message != Api.WM_MOUSEMOVE) return; p = mouse.xy; w.MoveL(p.x - offs.x, p.y - offs.y); //if (!canDock && keys.gui.isAlt) { // canDock = true; // //w.SetTransparency(true, 128); // //_dockIndic = new _DockIndicator(_manager, this); // //_dockIndic.Show(this); //} ////if (canDock) _dockIndic.OnFloatMoved(_manager.PointToClient(p)); }); //if (canDock) { // w.SetTransparency(false); // _dockIndic.Close(); // if (ok) { // target = _dockIndic.OnFloatDropped(); // } // _dockIndic = null; //} //return target; } public void ChangeWindowStyle(_WindowStyle style) { bool on = _node._windowStyle.Has(style); var w = this.Hwnd(); if (style == _WindowStyle.Topmost) { bool unowned = _node._windowStyle.Has(_WindowStyle.Unowned); if (on) { if (!unowned) base.Owner = null; w.ZorderTopmost(); } else { w.ZorderNoTopmost(); if (!unowned) base.Owner = _owner; } } else if (style == _WindowStyle.Unowned) { if (!on) w.ShowNotMinMax(); bool topmost = _node._windowStyle.Has(_WindowStyle.Topmost); if (!topmost) base.Owner = on ? null : _owner; //or `WndUtil.SetOwnerWindow(w, on ? default : _owner.Hwnd());` w.SetExStyle(WSE.APPWINDOW, on ? WSFlags.Add : WSFlags.Remove); //w.SetExStyle(w.ExStyle & ~(WSE.TOOLWINDOW | WSE.APPWINDOW) | (on ? WSE.APPWINDOW : WSE.TOOLWINDOW)); var ws = WS.CAPTION | WS.MAXIMIZEBOX | WS.MINIMIZEBOX | WS.SYSMENU; if (!on && _node._IsStack) ws &= ~WS.CAPTION; w.SetStyle(ws, on ? WSFlags.Add | WSFlags.UpdateNonclient : WSFlags.Remove | WSFlags.UpdateNonclient); base.ShowInTaskbar = on; } } } public Func DontActivateFloating { get; set; } } } ================================================ FILE: Au.Controls/KPanels/_Node.cs ================================================ using System.Xml.Linq; using System.Xml; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Shapes; namespace Au.Controls; public partial class KPanels { partial class _Node : TreeBase<_Node>, ILeaf { readonly KPanels _pm; readonly _StackFields _stack; readonly _TabFields _tab; readonly _LeafFields _leaf; readonly FrameworkElement _elem; //_stack.grid or _tab.tc or _leaf.panel GridSplitter2 _splitter; //splitter before this node in stack. null if in tab or first in stack readonly LeafType _leafType; int _index; //index in parent stack or tab. Grid row/column index is _index*2, because at _index*2-1 is splitter if _index>0. GridLength _dockedSize; Dock _headerAt; _DockState _state, _savedDockState; _Floating _floatWindow; string _floatSavedRect; _WindowStyle _windowStyle; //_Flags _flags; bool _dontSave; static readonly Brush s_toolbarHeaderBrush = SystemColors.ControlBrush; const int c_minSize = 4; const int c_defaultSplitterSize = 4; class _StackFields { public Grid grid; public bool isVertical; //vertical stack with horizontal splitters } class _TabFields { public _TabControl tc; public bool isVerticalHeader; //vertical buttons at left/right } class _LeafFields { public _DockPanelWithBorder panel; public FrameworkElement content; //app sets it = any element public FrameworkElement header; //TextBlock if panel/userdocument, Rectangle if toolbar/documentplaceholder. null if in tab. public string name; //used by the indexer to find it, also as header/tabitem text public bool addedLater; //added with AddSibling public bool canClose; //AddSibling(canClose). Adds context menu item "Close". public bool isExtension; //AddSibling(isExtension). Saves. } [Flags] enum _Flags { Splitter_ResizeNearest = 1 } /// /// Used to create root node when loading from XML. /// public _Node(KPanels pm, XElement x) : this(pm, x, null, 0) { } /// /// Used to create nodes when loading from XML. /// _Node(KPanels pm, XElement x, _Node parent, int index) { _pm = pm; _index = index; string tag = x.Name.LocalName; if (parent == null) { //the root XML element if (tag != "stack") throw new ArgumentException("XML root element must be 'stack'"); _pm._rootStack = this; } else { parent.AddChild(this); } switch (tag) { case "stack": _stack = new _StackFields { isVertical = x.Attr("o") == "v" }; _elem = _stack.grid = new Grid(); break; case "tab": _tab = new(); _elem = _tab.tc = new _TabControl(); break; case "panel": case "toolbar": case "document": _leaf = new(); _elem = _leaf.panel = new(); _leafType = tag[0] switch { 'p' => LeafType.Panel, 't' => LeafType.Toolbar, _ => LeafType.Document }; _leaf.name = x.Attr("name") ?? throw new ArgumentException("XML element without 'name'"); _leaf.isExtension = x.HasAttr("ext"); _Dictionary.Add(_leaf.name, this); break; default: throw new ArgumentException("unknown XML tag: " + tag); } _elem.UseLayoutRounding = true; _elem.Tag = this; if (parent != null) { if (!_IsDocument) { _savedDockState = (_DockState)(x.Attr("state", 0) & 3); _floatSavedRect = x.Attr("floatRect"); x.Attr(out _windowStyle, "window"); } if (!_IsStack) x.Attr(out _headerAt, "headerAt"); if (_ParentIsStack) { _dockedSize = _GridLengthFromString(x.Attr("z")); //height in vertical stack or width in horizontal stack _AddToStack(moving: false, _index == 0 ? 0 : x.Attr("s", c_defaultSplitterSize)); var flags = (_Flags)x.Attr("flags", 0); if (flags.Has(_Flags.Splitter_ResizeNearest) && _splitter != null) _splitter.ResizeNearest = true; if (_IsTab) _InitTabControl(); } else { _AddToTab(moving: false); } } if (!_IsLeaf) { //stack or tab if (_ParentIsTab) throw new ArgumentException(tag + " in 'tab'"); int i = 0; foreach (var e in x.Elements()) { new _Node(_pm, e, this, i++); } Debug.Assert(i > 0); if (_IsTab && i > 0) { _tab.tc.SelectedIndex = Math.Clamp(x.Attr("active", 0), 0, i - 1); } } //print.it(new string('\t', parent?.Level ?? 0) + _ntype, _ptype, Name, _indexInParent); if (parent == null) { //the root XML element int nVisible = 0; _Node firstHidden = null; List<_Node> aFloat = null; foreach (var v in Descendants()) { if (!v._IsStack) if (v._IsVisibleReally(true)) nVisible++; else firstHidden ??= v; var ds = v._savedDockState; if (ds == _DockState.Float) (aFloat ??= new()).Add(v); else if (ds != 0) v._SetDockState(ds); } if (nVisible == 0 && firstHidden != null) { //if all non-stack hidden, unhide one, else user cannot unhide any because there are no headers to show the context menu firstHidden._SetDockState(0); } if (aFloat != null) { DependencyPropertyChangedEventHandler eh = null; eh = (_, e) => { if (e.NewValue is bool visible && visible) { _stack.grid.IsVisibleChanged -= eh; _stack.grid.Dispatcher.InvokeAsync(() => { foreach (var v in aFloat) { if (v._state == _DockState.Hide) { v._state |= _DockState.Float; continue; } //hidden after loading v._SetDockState(_DockState.Float); } }); } }; _stack.grid.IsVisibleChanged += eh; } //timer.after(1000, _ => _Test(5)); ////timer.after(5000, _ => _Test(0)); //void _Test(int margin) { // foreach (var v in Descendants(true)) { // if (v._IsStack) v._stack.grid.Background = (v.Level & 3) switch { 0 => Brushes.CornflowerBlue, 1 => Brushes.Khaki, 2 => Brushes.YellowGreen, _ => Brushes.LightYellow }; // if(v!=this) v._elem.Margin = new Thickness(margin); // if (v._splitter != null) v._splitter.Visibility = Visibility.Collapsed; // } //} ////_stack.grid.PreviewMouseMove += _RootGrid_PreviewMouseMove; } } /// /// Used when moving a node, to create new parent (this) stack or tab for it and target. /// Also when moving a node, to create new parent (this) tab for it (target). /// _Node(_Node target, bool isTab, bool verticalStack = false) { _pm = target._pm; bool targetIsRoot = !isTab && target.Parent == null; if (targetIsRoot) { _pm._rootStack = this; } else { _index = target._index; target.AddSibling(this, after: false); target.Remove(); target._index = 0; } AddChild(target); if (isTab) { _tab = new(); _elem = _tab.tc = new _TabControl(); } else { _stack = new _StackFields { isVertical = verticalStack }; _elem = _stack.grid = new Grid(); } _elem.Tag = this; if (targetIsRoot) { //target._dockedSize = ...; //_AddToParentWhenMovingOrAddingLater will set it for target and this _pm._setContainer(_stack.grid); } else { _ReplaceInStack(target); if (isTab) { _headerAt = target._headerAt; _InitTabControl(); } } } /// /// Used when creating new leaf node later (after loading). /// _Node(_Node target, bool after, LeafType type, string name, bool canClose, bool isExtension) { _pm = target._pm; _leaf = new() { addedLater = true, name = name, canClose = canClose, isExtension = isExtension }; _elem = _leaf.panel = new() { Tag = this, UseLayoutRounding = true }; _leafType = type; _dontSave = !isExtension; _Dictionary.Add(name, this); _AddToParentWhenMovingOrAddingLater(target, after); } public void Save(XmlWriter x) { if (Parent == null) { //mark to not save stack/tab nodes without savable leaf descendants _Children(this); static bool _Children(_Node p) { bool R = false; foreach (var v in p.Children()) { if (!v._IsLeaf) v._dontSave = !_Children(v); R |= !v._dontSave; } return R; } } else { if (_dontSave) return; } x.WriteStartElement(_IsStack ? "stack" : (_IsTab ? "tab" : (_IsToolbar ? "toolbar" : (_IsDocument ? "document" : "panel")))); if (_IsStack) x.WriteAttributeString("o", _stack.isVertical ? "v" : "h"); if (Parent != null) { if (_IsLeaf) { x.WriteAttributeString("name", _leaf.name); if (_leaf.isExtension) x.WriteAttributeString("ext", ""); } if (_ParentIsStack) { if (!_dockedSize.IsAuto) { if (_IsDockedInStack) _dockedSize = _SizeDef; //update _dockedSize x.WriteAttributeString("z", _GridLengthToString(_dockedSize)); } var z = _SplitterSize; if (z > 0 && z != c_defaultSplitterSize) x.WriteAttributeString("s", z.ToString()); if (_splitter != null && _splitter.ResizeNearest) x.WriteAttributeString("flags", ((int)_Flags.Splitter_ResizeNearest).ToString()); } if (_IsTab) { int i = _tab.tc.SelectedIndex; if (i > 0) x.WriteAttributeString("active", i.ToString()); } if (_headerAt != 0) x.WriteAttributeString("headerAt", _headerAt.ToString()); if (!_IsDocument) { if (_state != 0) x.WriteAttributeString("state", ((int)_state).ToString()); _floatWindow?.Save(); if (_floatSavedRect != null) x.WriteAttributeString("floatRect", _floatSavedRect); if (_windowStyle != 0) x.WriteAttributeString("window", _windowStyle.ToString()); } } if (!_IsLeaf) foreach (var v in Children()) v.Save(x); x.WriteEndElement(); } bool _IsStack => _stack != null; bool _IsTab => _tab != null; bool _IsLeaf => _leaf != null; bool _IsPanel => _leafType == LeafType.Panel; bool _IsToolbar => _leafType == LeafType.Toolbar; bool _IsDocument => _leafType == LeafType.Document; bool _ParentIsStack => Parent?._IsStack ?? false; bool _ParentIsTab => Parent?._IsTab ?? false; bool _ParentIsTabAndNotFloating => _ParentIsTab && _state != _DockState.Float; /// /// true if this is toolbar or this is stack/tab containing only toolbars. /// bool _IsToolbarsNode => _IsToolbar || (!_IsLeaf && Descendants().All(o => o._IsToolbar)); /// /// true if this is document or this is tab containing documents (or tab with 0 children, which normally is not possible). /// bool _IsDocumentsNode => _IsDocument || (_IsTab && (FirstChild?._IsDocument ?? true)); Dictionary _Dictionary => (_leaf.addedLater && _IsDocument) ? _pm._dictUserDoc : _pm._dictLeaf; /// /// Gets name of panel/toolbar/document. Exception if not leaf. /// public string Name => _leaf.name; /// /// Gets the UI element of this node. It is Grid if this is stack, or TabControl if tab, else DockPanel. /// public FrameworkElement Elem => _elem; public override string ToString() { string s; if (_IsLeaf) { s = Name; if (_IsToolbar) s = "tb " + Name; return s; } if (Parent == null) return "Root stack"; var a = Descendants().Where(o => o._IsLeaf).ToArray(); s = a.Length switch { 0 => "", 1 => a[0].ToString(), 2 => a[0].ToString() + ", " + a[1].ToString(), _ => a[0].ToString() + " ... " + a[^1].ToString() }; return (_IsTab ? "Tabs {" : "Stack {") + s + "}"; } //string _ToStringWithoutTB() { // var s = ToString(); // if (_IsToolbar) s = s[3..]; // return s; //} /// /// true if _IsPanel or (_IsDocument and _leaf.addedLater). /// bool _CanHaveHeaderWithText => _IsPanel || (_IsDocument && _leaf.addedLater); void _SetHeaderAt(Dock ca, bool firstTime = false) { //if (_ParentIsTab) { // Parent._SetHeaderAt(ca, firstTime); // return; //} Dock old = firstTime ? Dock.Top : _headerAt; _headerAt = ca; if (_IsTab) { var tc = _tab.tc; if (ca == tc.TabStripPlacement) return; if (_tab.isVerticalHeader) { _tab.isVerticalHeader = false; foreach (TabItem v in tc.Items) v.Style = null; } tc.TabStripPlacement = ca; _VerticalTabHeader(); } else if (_leaf.header != null) { DockPanel.SetDock(_leaf.header, ca); if (_leaf.content != null) _SetToolbarOrientation(); if (ca == old || !_CanHaveHeaderWithText) return; if (ca == Dock.Top || ca == Dock.Bottom) { if (old == Dock.Left || old == Dock.Right) _leaf.header.LayoutTransform = null; } else { _leaf.header.LayoutTransform = new RotateTransform(ca == Dock.Left ? 270d : 90d); } } } void _AddRemoveHeaderAndBorder() { if (!_IsLeaf) return; if (_ParentIsTab && !_state.Has(_DockState.Float)) { if (_leaf.header != null) { _leaf.panel.Children.Remove(_leaf.header); _leaf.header = null; _leaf.panel.BorderThickness = default; } } else { if (_leaf.header == null) { if (_CanHaveHeaderWithText) { _leaf.header = new TextBlock { Text = Name, TextAlignment = TextAlignment.Center, Padding = new Thickness(2, 1, 2, 3), Background = _pm.HeaderBrush, Foreground = Brushes.Black, TextTrimming = TextTrimming.CharacterEllipsis }; } else if (_IsToolbar) { var c = new Rectangle { MinHeight = 5, MinWidth = 5, Fill = s_toolbarHeaderBrush, //note: without Fill there are no events }; c.MouseEnter += (_, _) => c.Fill = _pm.HeaderBrush; c.MouseLeave += (_, _) => c.Fill = s_toolbarHeaderBrush; _leaf.header = c; } else { //document placeholder var c = new Rectangle { //MinHeight = 5, MinWidth = 5, //rejected. Let be 0 (no header). User could accidentally undock when trying to scroll. // Not tested headerless with tabbed documents. Then probably this code not used because _CanHaveHeaderWithText true. Fill = _pm.HeaderBrush, }; _leaf.header = c; _leaf.panel.LastChildFill = false; bool hasDoc = false; c.SizeChanged += (_, e) => { bool has = _leaf.panel.Children.Count > 1; if (has != hasDoc) _leaf.panel.LastChildFill = hasDoc = has; }; } _leaf.panel.Children.Insert(0, _leaf.header); _SetHeaderAt(_headerAt, true); _leaf.header.ContextMenuOpening += _HeaderContextMenu; _leaf.header.MouseDown += _OnMouseDown; if (_pm.BorderBrush != null && !_IsToolbar) { _leaf.panel.BorderBrush = _pm.BorderBrush; _leaf.panel.BorderThickness = new Thickness(1); } } } } void _SetToolbarOrientation() { if (_IsToolbar && _leaf.content is ToolBarTray t) { var ori = _headerAt == Dock.Top || _headerAt == Dock.Bottom ? Orientation.Vertical : Orientation.Horizontal; if (t.Orientation != ori) t.Orientation = ori; } } #region ILeaf FrameworkElement ILeaf.Content { get => _leaf.content; set { if (_leaf.content != null) _leaf.panel.Children.Remove(_leaf.content); _leaf.panel.Children.Add(_leaf.content = value); _SetToolbarOrientation(); } } bool ILeaf.Visible { get => _IsVisibleReally(); set { if (value && _savedDockState == _DockState.Float) return; //will make float async if (value) _Unhide(); else _Hide(); if (value && _ParentIsTabAndNotFloating) { Parent._tab.tc.SelectedIndex = _index; if (value && !_elem.IsLoaded) _elem.UpdateLayout(); //workaround: WPF creates HwndHost control handle async. Let's create now. } } } /// /// Returns true if this node does not have hidden state and is not docked in hidden tab. /// bool _IsVisibleReally(bool useSavedState = false) { var state = useSavedState ? _savedDockState : _state; if (state.Has(_DockState.Hide)) return false; if (state == 0 && _ParentIsTab) return Parent._IsVisibleReally(useSavedState); return true; } bool ILeaf.Floating { get => _state == _DockState.Float; set { if (value && _savedDockState == _DockState.Float) return; //will make float async _SetDockState(value ? _DockState.Float : _state & ~_DockState.Float); } } public Action DontFocusTab { get; set; } ParentInfo ILeaf.Parent => new ParentInfo(_leaf.panel.Panel, Parent._elem, _index); ILeaf ILeaf.AddSibling(bool after, LeafType type, string name, bool canClose, bool isExtension) { if (name == null || (type is not (LeafType.Panel or LeafType.Toolbar or LeafType.Document))) throw new ArgumentException(); return new _Node(this, after, type, name, canClose, isExtension); } public void Delete() { if (!_leaf.addedLater && !_leaf.isExtension) throw new InvalidOperationException(); if (_state == _DockState.Float) _SetDockState(0); var oldParent = Parent; _RemoveFromParentWhenMovingOrDeleting(); _RemoveParentIfNeedAfterMovingOrDeleting(oldParent); _Dictionary.Remove(_leaf.name); } void ILeaf.Rename(string name) { //if (!_IsLeaf) throw new InvalidOperationException(); //impossible, unless called from this class if (name == null) throw new ArgumentException(); _Dictionary.Remove(_leaf.name); _Dictionary.Add(_leaf.name = name, this); if (_leaf.header is TextBlock t) t.Text = name; if (_ParentIsTab) { (Parent._tab.tc.Items[_index] as TabItem).Header = name; Parent._VerticalTabHeader(onMove: true); } if (_floatWindow != null) _floatWindow.Title = name; } public event EventHandler VisibleChanged; public event EventHandler FloatingChanged; public event System.ComponentModel.CancelEventHandler Closing; //public event EventHandler ContextMenuOpening; public event EventHandler TabSelected; public event EventHandler ParentChanged; #endregion } class _DockPanelWithBorder : Border { public readonly DockPanel Panel; public _DockPanelWithBorder() { Child = Panel = new(); SnapsToDevicePixels = true; } public UIElementCollection Children => Panel.Children; public bool LastChildFill { get => Panel.LastChildFill; set => Panel.LastChildFill = value; } public new object Tag { get => base.Tag; set { base.Tag = value; Panel.Tag = value; } } } } ================================================ FILE: Au.Controls/KPanels/dock.cs ================================================ using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Controls.Primitives; namespace Au.Controls; public partial class KPanels { partial class _Node { [Flags] enum _DockState { Hide = 1, Float = 2, } [Flags] enum _WindowStyle { Topmost = 1, Unowned = 2, } void _HeaderContextMenu(object sender, ContextMenuEventArgs e) { if (!_IsGoodMouseEvent(sender, e, out var target)) return; e.Handled = true; target._HeaderContextMenu(this); } void _HeaderContextMenu(_Node thisOrParentTab) { if (_IsDocument && !_leaf.addedLater) return; var m = new popupMenu(); bool canClose = _leaf?.canClose ?? false; if (canClose) m["Close\tM-click"] = _ => _UserClosing(); _DockStateItem(_DockState.Hide, canClose ? "Hide" : "Hide\tM-click"); if (_state.Has(_DockState.Float)) _DockStateItem(0, "Dock\tD-click"); else _DockStateItem(_DockState.Float, "Float\tD-click, drag"); m.Submenu("Header at", m => { _HeaderAtItem(Dock.Left); _HeaderAtItem(Dock.Top); _HeaderAtItem(Dock.Right); _HeaderAtItem(Dock.Bottom); void _HeaderAtItem(Dock ca) { m.AddRadio(ca.ToString(), ca == thisOrParentTab._headerAt, o => thisOrParentTab._SetHeaderAt(ca)); } }); m.Submenu("Window", m => { _WindowItem(_WindowStyle.Topmost, "Topmost"); _WindowItem(_WindowStyle.Unowned, "Unowned, can min/max"); void _WindowItem(_WindowStyle ws, string s) { m.AddCheck(s, _windowStyle.Has(ws), o => { _windowStyle ^= ws; _floatWindow?.ChangeWindowStyle(ws); }); } }); _ContextMenu_Move(m); if (_leaf?.isExtension ?? false) { m["Remove..."] = o => { var s1 = _leafType == LeafType.Toolbar ? "toolbar" : "panel"; if (!dialog.showYesNo($"Remove this extension {s1}?", this.Name, owner: _pm._ContainerWindow)) return; Delete(); print.it($"Info: extension {s1} {this.Name} has been removed. To uninstall the extension, also remove its script from startup scripts in Options and delete the script."); }; } _ShowSubmenus(); //ContextMenuOpening?.Invoke(this, m); m.Show(owner: _pm._ContainerWindow); void _DockStateItem(_DockState state, string text) { m[text] = o => _SetDockState(state); } void _ShowSubmenus() { var a = new List<_Node>(); foreach (var v in RootAncestor.Descendants()) { if (v._IsStack || !v._state.Has(_DockState.Hide)) continue; if (v._IsTab && v.Children().All(o => o._state.Has(_DockState.Hide))) continue; a.Add(v); } if (a.Count == 0) return; m.Separator(); a.Sort((x, y) => { if (x._IsToolbar && !y._IsToolbar) return -1; if (y._IsToolbar && !x._IsToolbar) return 1; return string.Compare(x.ToString(), y.ToString(), true); }); m.Submenu("Show", m => { int i = 0; foreach (var v in a) { if (i > 0 && a[i - 1]._IsToolbar != a[i]._IsToolbar) m.Separator(); i++; m[v.ToString()] = _ => v._Unhide(); } #if DEBUG if (a.Count > 1) { m.Separator(); m["Show all (debug)"] = _ => { foreach (var v in a) v._Unhide(); }; } #endif }); } #if DEBUG m.Separator(); m.Submenu("Debug", m => { m["Invalidate window"] = _ => _Invalidate(_pm._ContainerWindow); m["Invalidate floats"] = _ => { foreach (var v in RootAncestor.Descendants()) { if (v._floatWindow != null) _Invalidate(v._floatWindow); } }; m.Add(0, "info: toggle ScrollLock if does not work", disable: true); }); void _Invalidate(Window w) { //if (keys.isScrollLock) Api.InvalidateRect(w.Hwnd(), IntPtr.Zero, true); //works ////else w.UpdateLayout(); //no //else w.InvalidateVisual(); //no Api.InvalidateRect(w.Hwnd(), keys.isScrollLock); } #endif } private protected void _OnMouseDown(object sender, MouseButtonEventArgs e) { switch (e.ChangedButton) { case MouseButton.Left: case MouseButton.Middle: break; default: return; } if (_IsGoodMouseEvent(sender, e, out var target)) target._OnMouseDown(e); } void _OnMouseDown(MouseButtonEventArgs e) { if (_IsStack) { if (!(e.ChangedButton == MouseButton.Left && e.ClickCount == 1 && Keyboard.Modifiers == ModifierKeys.Alt)) return; if (Parent == null) { e.Handled = true; return; } } e.Handled = true; if (e.ChangedButton == MouseButton.Left) { if (e.ClickCount == 1) { e.Handled = false; //if tab item, let select it timer.after(1, _ => { //Dispatcher.InvokeAsync does not work POINT p = mouse.xy; if (Api.DragDetect(_elem.Hwnd(), p)) { _SetDockState(_DockState.Float, onDrag: true); _floatWindow?.Drag(p); } }); } else if (e.ClickCount == 2) { _SetDockState(_state ^ _DockState.Float); } } else { if (_leaf?.canClose ?? false) _UserClosing(); else _Hide(); } } bool _IsGoodMouseEvent(object sender, RoutedEventArgs e, out _Node target) { target = null; if (e.Source == sender) { if (_IsTab && e.OriginalSource is not FlexStackPanel) return false; //tab control border target = sender == _splitter ? Parent : this; } else if (e.Source is TabItem ti && ti.Parent == sender) target = _NodeFromTabItem(ti); else return false; return true; } void _UserClosing() { if (Closing != null) { var e = new CancelEventArgs(); Closing(this, e); if (e.Cancel) return; } Delete(); } void _Hide() => _SetDockState(_DockState.Hide); void _Unhide() => _SetDockState(_state & ~_DockState.Hide); void _SetDockState(_DockState state, bool onDrag = false) { //print.qm2.write(this, state, " ", _state); _savedDockState = 0; if (state == _DockState.Hide) state |= _state & _DockState.Float; if (state == _state) { if (state == 0 && Parent._state.Has(_DockState.Hide)) Parent._Unhide(); //in hidden tab return; } var oldState = _state; _state = state; if (oldState == _DockState.Float) { _floatWindow?.Close(); //_floatWindow sets _floatWindow=null when closing } else if (state == _DockState.Float) { _floatWindow = new _Floating(this, onDrag); //ctor uses docked rect } if (state == 0) { //dock; was hidden or floating _AddRemoveHeaderAndBorder(); if (_ParentIsTab) _ShowHideInTab(true); else _ShowHideInStack(true); } else { if (oldState == 0) { //was docked; now hide or float if (_ParentIsTab) _ShowHideInTab(false); else _ShowHideInStack(false); } _AddRemoveHeaderAndBorder(); if (state == _DockState.Float) { _floatWindow.Content = _elem; _floatWindow.ShowIfOwnerVisible(); } } if ((state ^ oldState).Has(_DockState.Hide)) VisibleChanged?.Invoke(this, oldState.Has(_DockState.Hide)); if ((state ^ oldState).Has(_DockState.Float)) FloatingChanged?.Invoke(this, state.Has(_DockState.Float)); } void _ContextMenu_Move(popupMenu m) { m.Submenu("Move to", m => { m.RawText = true; string sThis = ToString(); foreach (var target in RootAncestor.Descendants(andSelf: true)) { bool targetInTab = target._ParentIsTab; if (targetInTab) { if (!_IsLeaf || target._IsDocument != _IsDocument) continue; } else if (_IsDocument && _ParentIsTab) { //allow only beside parent tab or in/besides another document or doc tab. Elsewhere probably not useful, just adds many menu items. if (target != Parent && !target._IsDocumentsNode) continue; } if (target.Ancestors(andSelf: true).Contains(this)) continue; string sTarget = target.ToString(); var s1 = new string(' ', target.Level * 4) + sTarget + (target._state switch { 0 => null, _DockState.Float => " (floating)", _ => " (hidden)" }); m.Submenu(s1, m => { bool sep = false; //this would be duplicate of before/after //if (target._IsStack || (target._IsTab && _IsLeaf && (target.FirstChild?._IsDocument ?? false) == _IsDocument)) { // int i = 0; // foreach (var u in target.Children()) { // m[i++ == 0 ? "First" : ($"Before '{u}'")] = o => _MoveTo(u, _HowToMove.BeforeTarget); // } // m["Last"] = o => _MoveTo(target.LastChild, _HowToMove.AfterTarget); // sep = true; //} if (target.Parent != null) { //if (sep) m.Separator(); if (target.Previous != this) _AddMI($"Before '{sTarget}'", _HowToMove.BeforeTarget); if (target.Next != this) _AddMI($"After '{sTarget}'", _HowToMove.AfterTarget); sep = true; } if (!targetInTab) { if (sep) m.Separator(); if (target._IsLeaf && _IsLeaf && target._IsDocument == _IsDocument) { m.Add(0, $"Create tabs and add '{sThis}' as:", disable: true); _AddMI($"- First tab (before '{sTarget}')", _HowToMove.FirstInNewTab); _AddMI($"- Last tab (after '{sTarget}')", _HowToMove.LastInNewTab); m.Separator(); } m.Add(0, $"Create stack and add '{sThis}' at:", disable: true); _AddMI("- Left", _HowToMove.NewStack, Dock.Left); _AddMI("- Right", _HowToMove.NewStack, Dock.Right); _AddMI("- Top", _HowToMove.NewStack, Dock.Top); _AddMI("- Bottom", _HowToMove.NewStack, Dock.Bottom); } if (target._IsStack || (target._IsTab && _IsLeaf) && target.FirstChild == null) { //empty m.Separator(); _AddMI($"- Into '{sTarget}'", _HowToMove.Child); } void _AddMI(string text, _HowToMove how, Dock dock = default) { _MoveRect k = new(target, how, dock); m.Add(text, o => _MoveTo(k.target, k.how, k.dock)).Tag = k; } _RectTimer(m, true); }).Tag = target; } _RectTimer(m, false); static void _RectTimer(popupMenu m, bool second) { var osd = new osdRect { Color = second ? 0x60c000 : 0x4040ff }; if (second) osd.Opacity = .5; PMItem pmi = null; timer.every(100, t => { if (m.IsOpen) { var mi = m.FocusedItem; if (mi != pmi) { pmi = mi; bool visible = false; if (mi != null) { if (!second) { var n = mi.Tag as _Node; if (visible = _GetRectInScreen(n, out var r)) osd.Rect = r; } else if (mi.Tag is _MoveRect k) { var n = k.target; if (n._floatWindow == null) if (visible = _GetRectInScreen(n, out var r)) { r.Inflate(-osd.Thickness, -osd.Thickness); if (k.how is _HowToMove.BeforeTarget or _HowToMove.AfterTarget) { bool vert = n._ParentIsTab ? n.Parent._headerAt is Dock.Left or Dock.Right : n.Parent._stack.isVertical; if (k.how == _HowToMove.BeforeTarget) { if (vert) r.bottom = r.top + r.Height / 2; else r.right = r.left + r.Width / 2; } else { if (vert) r.top = r.bottom - r.Height / 2; else r.left = r.right - r.Width / 2; } } else if (k.how == _HowToMove.Child) { if (n._stack.isVertical) r.bottom = r.top + r.Height / 4; else r.right = r.left + r.Width / 4; } else if (k.how == _HowToMove.NewStack) { if (k.dock == Dock.Left) r.right = r.left + r.Width / 2; if (k.dock == Dock.Right) r.left = r.right - r.Width / 2; if (k.dock == Dock.Top) r.bottom = r.top + r.Height / 2; if (k.dock == Dock.Bottom) r.top = r.bottom - r.Height / 2; } osd.Rect = r; } } } osd.Visible = visible; } } else { t.Stop(); osd.Dispose(); } }); } static bool _GetRectInScreen(_Node n, out RECT r) { if (n._IsVisibleReally()) { FrameworkElement e = n._IsStack ? n._stack.grid : n._IsTab ? n._tab.tc : n._elem.Parent is TabItem ti ? ti : n._leaf.panel.Panel; try { r = e.RectInScreen(); return true; } catch (Exception) { } } r = default; return false; } }); } enum _HowToMove //don't reorder { BeforeTarget, //before target in parent stack or tab AfterTarget, //after target in parent stack or tab NewStack, //create new stack in place of target; add target and this to it; use dock to set orientation of new stack and index ot this and target FirstInNewTab, //create new tab in place of target; add this (first) and target to it LastInNewTab, //create new tab in place of target; add target and this (last) to it Child, //add as child of target (target is empty stack or tab) } record class _MoveRect(_Node target, _HowToMove how, Dock dock = default); void _MoveTo(_Node target, _HowToMove how, Dock dock = default) { if (target == this) return; bool beforeAfter = how <= _HowToMove.AfterTarget; if (_state != 0) _SetDockState(0); if (target._state != 0 && !beforeAfter) target._SetDockState(0); bool after = how == _HowToMove.AfterTarget; var oldParent = Parent; if (beforeAfter && target.Parent == oldParent && oldParent._IsTab) { //just reorder buttons _ReorderInTab(target, after); return; } _RemoveFromParentWhenMovingOrDeleting(); switch (how) { case _HowToMove.NewStack: new _Node(target, isTab: false, verticalStack: dock == Dock.Top || dock == Dock.Bottom); target._AddToStack(moving: false, c_defaultSplitterSize); after = dock == Dock.Right || dock == Dock.Bottom; break; case _HowToMove.FirstInNewTab: case _HowToMove.LastInNewTab: new _Node(target, isTab: true); target._AddToTab(moving: false); after = how == _HowToMove.LastInNewTab; break; } if (how == _HowToMove.Child) _AddToParentWhenMoving(target); else _AddToParentWhenMovingOrAddingLater(target, after); #if false //debug print int i = 0; foreach (var v in Parent.Children()) { print.it(i++, v._index, v); if (Parent._IsStack) { if (v._splitter != null) print.it("splitter", _RC(v._splitter)); print.it("elem ", _RC(v._elem), v._dockedSize, v._SizeDef); int _RC(FrameworkElement e) => Parent._stack.isVertical ? Grid.GetRow(e) : Grid.GetColumn(e); } } #endif _RemoveParentIfNeedAfterMovingOrDeleting(oldParent); if (how <= _HowToMove.NewStack && _IsDocument && oldParent._IsTab && !_ParentIsTab) { _headerAt = oldParent._headerAt; new _Node(this, isTab: true); _AddToTab(moving: false); } if (Parent != oldParent) ParentChanged?.Invoke(this, EventArgs.Empty); } void _AddToParentWhenMovingOrAddingLater(_Node target, bool after) { target.AddSibling(this, after); _index = target._index + (after ? 1 : 0); if (_ParentIsTab) { _AddToTab(moving: true); _AddRemoveHeaderAndBorder(); //if(select) Parent._tab.tc.SelectedIndex = _index; } else { if (!(_dockedSize.IsAuto && _IsToolbarsNode)) _dockedSize = new GridLength(100, GridUnitType.Star); if (Parent.Count == 2) target._SizeDef = target._dockedSize = new GridLength(100, GridUnitType.Star); _AddToStack(moving: true, c_defaultSplitterSize); } } void _AddToParentWhenMoving(_Node parent) { parent.AddChild(this, first: true); _index = 0; if (_ParentIsTab) { _AddToTab(moving: true); _AddRemoveHeaderAndBorder(); Parent._tab.tc.SelectedIndex = _index; } else { if (!(_dockedSize.IsAuto && _IsToolbarsNode)) _dockedSize = new GridLength(100, GridUnitType.Star); _AddToStack(moving: true, c_defaultSplitterSize); } } void _RemoveFromParentWhenMovingOrDeleting() { if (Parent._IsStack) { _RemoveGridRowCol(_elem); _RemoveSplitter(); if (_index == 0) Next?._RemoveSplitter(); } else { if (_elem.Parent is TabItem ti) { //null if hidden or floating ti.Content = null; Parent._tab.tc.Items.Remove(ti); } } _ShiftSiblingIndices(-1); Remove(); } void _RemoveParentIfNeedAfterMovingOrDeleting(_Node oldParent) { int n = oldParent.Count; if (n == 0) { var pp = oldParent.Parent; oldParent._RemoveFromParentWhenMovingOrDeleting(); oldParent._RemoveParentIfNeedAfterMovingOrDeleting(pp); } else if (n == 1) { if (!_IsDocument) { var f = oldParent.FirstChild; if (oldParent.Parent != null) { f._MoveTo(oldParent, _HowToMove.BeforeTarget); } else { f._RemoveFromParentWhenMovingOrDeleting(); _pm._rootStack = f; _pm._setContainer(f._stack.grid); } } } else if (oldParent._IsTab && Parent != oldParent) { oldParent._VerticalTabHeader(onMove: true); } } void _ShiftSiblingIndices(int n) { for (var v = this; (v = v.Next) != null;) v._index += n; } } [Conditional("DEBUG")] internal void PrintTree_(string header = null) { print.qm2.write("-----" + header); foreach (var v in _rootStack.Descendants(true)) { print.qm2.write(new string('\t', v.Level) + v); } } } ================================================ FILE: Au.Controls/KPanels/stack.cs ================================================ using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace Au.Controls; public partial class KPanels { partial class _Node { /// /// Gets nodes docked and really visible in this stack (not in tab, not floating, not hidden). /// List<_Node> _Stack_DockedNodes { get { var a = new List<_Node>(); foreach (var v in Children()) if (v._elem.Parent == _stack.grid) a.Add(v); return a; } } /// /// true if parent is stack and this is docked and not floating/hidden. /// bool _IsDockedInStack => _ParentIsStack && _elem.Parent == Parent._stack.grid; _Node _PreviousDockedInStack { get { for (var v = this; (v = v.Previous) != null;) if (v._IsDockedInStack) return v; return null; } } //not used //_Node _NextDockedInStack { // get { // for (var v = this; (v = v.Next) != null;) if (v._IsDockedInStack) return v; // return null; // } //} /// /// Adds elements of this to parent stack. Creates splitter if need, adds row/column, sets element properties (header etc). /// /// Called when moving this. Inserts, shifts sibling indices, etc. If false, adds as last item in stack. /// void _AddToStack(bool moving, int splitterSize) { int i = _index * 2; if (i > 0) _CreateSplitter(splitterSize); var pstack = Parent._stack; var g = pstack.grid; if (pstack.isVertical) { g.InsertRow(i, new RowDefinition { Height = _dockedSize, MinHeight = c_minSize }); g.AddChild(_elem, i, 0); } else { g.InsertColumn(i, new ColumnDefinition { Width = _dockedSize, MinWidth = c_minSize }); g.AddChild(_elem, 0, i); } if (moving) { _ShiftSiblingIndices(1); var next = Next; if (next != null) { if (next._splitter == null) next._CreateSplitter(c_defaultSplitterSize); //workaround for: when moving a fixed-size item, 1 pixel of splitter after it may stop working. If splitter size is 1 pixel... else if (next._SplitterSize < c_defaultSplitterSize) next._SplitterSize = c_defaultSplitterSize; } } _AddRemoveHeaderAndBorder(); } /// /// Replaces target with this in parent stack. /// Used when moving, to create new parent (this) tab or stack for target and the moved node. /// Does not add/remove tree nodes. /// void _ReplaceInStack(_Node target) { if (target._splitter != null) { target._SetSplitterEvents(false); _splitter = target._splitter; target._splitter = null; _SetSplitterEvents(true); } _dockedSize = target._dockedSize; if (_dockedSize.IsAuto) _SizeDef = _dockedSize = new GridLength(100, GridUnitType.Star); int i = _index * 2; var pstack = Parent._stack; var g = pstack.grid; if (pstack.isVertical) Grid.SetRow(_elem, i); else Grid.SetColumn(_elem, i); g.Children.Remove(target._elem); g.Children.Add(_elem); _AddRemoveHeaderAndBorder(); target._AddRemoveHeaderAndBorder(); } /// /// Removes an element of this from parent stack grid. Removes its row/column and shifts sibling indices. /// /// _elem or _splitter void _RemoveGridRowCol(FrameworkElement e) { var pstack = Parent._stack; if (pstack.isVertical) pstack.grid.RemoveRow(e, false); else pstack.grid.RemoveColumn(e, false); } void _ShowHideInStack(bool show) { var g = Parent._stack.grid; var a = Parent._Stack_DockedNodes; if (!show) { _dockedSize = _SizeDef; g.Children.Remove(_elem); if (_splitter != null) _splitter.Visibility = Visibility.Collapsed; _SizeMin = 0; _SizeDef = default; //Auto a.Remove(this); if (a.Count == 0) { if (Parent._state == _DockState.Float) Parent._Hide(); else Parent._ShowHideInStack(show); } else if (_dockedSize.IsStar && !a.Any(o => o._SizeDef.IsStar)) { //if hiding last star-sized, make the last visible fixed node star-sized a.LastOrDefault(o => o._SizeDef.IsAbsolute)?._ChangeSizeUnit(GridUnitType.Star, false); //never mind: later should restore. It makes everything complicated. This probably is rare, and it's easy for user to set fixed size again. } } else { if (a.Count == 0) { if (Parent._state.Has(_DockState.Float)) Parent._SetDockState(_DockState.Float); else Parent._ShowHideInStack(show); } _SizeDef = _dockedSize; _SizeMin = c_minSize; g.Children.Add(_elem); } Parent._Stack_UpdateSplittersVisibility(); } #region splitter void _CreateSplitter(int size) { bool verticalStack = Parent._stack.isVertical; var c = new GridSplitter2(); if (verticalStack) { //horz splitter c.ResizeDirection = GridResizeDirection.Rows; c.VerticalAlignment = VerticalAlignment.Top; c.HorizontalAlignment = HorizontalAlignment.Stretch; } else { //vert splitter c.ResizeDirection = GridResizeDirection.Columns; c.HorizontalAlignment = HorizontalAlignment.Left; //default stretch } if (_pm.SplitterBrush != null) c.Background = _pm.SplitterBrush; _splitter = c; _SplitterSize = size; var g = Parent._stack.grid; int i = _index * 2 - 1; if (verticalStack) { g.InsertRow(i, new RowDefinition { Height = default }); //Auto g.AddChild(c, i, 0); } else { g.InsertColumn(i, new ColumnDefinition { Width = default }); g.AddChild(c, 0, i); } _SetSplitterEvents(true); } void _SetSplitterEvents(bool add) { if (add) { _splitter.ContextMenuOpening += _SplitterContextMenu; _splitter.PreviewMouseDown += _OnMouseDown; } else { _splitter.ContextMenuOpening -= _SplitterContextMenu; _splitter.PreviewMouseDown -= _OnMouseDown; } } /// /// This must be stack. Hides splitter of first visible child and shows splitters of other visible children. /// void _Stack_UpdateSplittersVisibility() { var a = _Stack_DockedNodes; for (int i = 0; i < a.Count; i++) { var v = a[i]; if (i > 0) v._splitter.Visibility = Visibility.Visible; else if (v._splitter != null) v._splitter.Visibility = Visibility.Collapsed; } } void _RemoveSplitter() { if (_splitter != null) { _RemoveGridRowCol(_splitter); _splitter = null; } } /// /// Gets or sets actual height of in vertical stack or width in horizontal stack. /// int _SplitterSize { get { if (_splitter == null) return 0; return (Parent._stack.isVertical ? _splitter.ActualHeight : _splitter.ActualWidth).ToInt(); } set { if (_splitter == null) return; if (Parent._stack.isVertical) _splitter.Height = value; else _splitter.Width = value; } } void _SplitterContextMenu(object sender, ContextMenuEventArgs e) { e.Handled = true; var parentStack = Parent._stack; var m = new popupMenu(); m.Submenu("Splitter size", m => { int z = _SplitterSize; for (int i = 1; i <= 10; i++) { m.AddRadio(i.ToString(), i == z, o => _SplitterSize = o.Text.ToInt()); } }); m.Separator(); bool vert = parentStack.isVertical; _PreviousDockedInStack._SplitterContextMenu_Unit(m, vert ? "Top" : "Left"); _SplitterContextMenu_Unit(m, vert ? "Bottom" : "Right"); if ((parentStack.isVertical ? parentStack.grid.RowDefinitions.Where(o => !o.Height.IsAuto).Count() : parentStack.grid.ColumnDefinitions.Where(o => !o.Width.IsAuto).Count()) > 2) { //m.Separator(); m.AddCheck("Resize Nearest\tCtrl", _splitter.ResizeNearest, _ => _splitter.ResizeNearest ^= true); } if (Parent.Parent != null) { m.Separator(); m.Submenu("Stack", m => { bool isFloating = Parent._state.Has(_DockState.Float); m[isFloating ? "Dock" : "Float\tAlt+drag"] = o => Parent._SetDockState(isFloating ? 0 : _DockState.Float); Parent._ContextMenu_Move(m); }); } timer.after(100, _ => Mouse.SetCursor(Cursors.Arrow)); //workaround. 30 too small, 50 ok m.Show(); } void _SplitterContextMenu_Unit(popupMenu m, string s1) { var unitNow = _SizeDef.GridUnitType; //var unitNow = _dockedSize.GridUnitType; //print.it(this, unitNow); bool allToolbars = _IsToolbarsNode; bool disableFixed = !allToolbars && unitNow != GridUnitType.Pixel && Parent.Children().Count(o => o._SizeDef.GridUnitType == GridUnitType.Star) < (unitNow == GridUnitType.Star ? 2 : 1); _UnitItem(s1 + " fixed", GridUnitType.Pixel); if (allToolbars) _UnitItem(s1 + " auto", GridUnitType.Auto); void _UnitItem(string text, GridUnitType unit) { m.AddCheck(text, unit == unitNow, o => _SetUnit(unit), disable: disableFixed && unit == GridUnitType.Pixel); //CONSIDER: don't disable. Maybe user wants to set row A fixed and then row B star, not vice versa. But if forgets and makes window smaller, some panels and splitters may become invisible. } void _SetUnit(GridUnitType unit) { if (unit == _SizeDef.GridUnitType) unit = GridUnitType.Star; //unchecking Fixed or Auto _ChangeSizeUnit(unit, true); } } #endregion #region row/col, size //void _NormalizeChildStars(bool percent) {//FUTURE: remove if unused. Not sure it is finished. // var e = Children(); // double sizeOfStars = percent ? e.Sum(v => !v._dockedSize.IsStar ? 0 : v._SizeNowDockedOrNot) : 0; // foreach (var v in e) { // if (!v._dockedSize.IsStar) continue; // var z = _SizeNowDockedOrNot; // if (percent && sizeOfStars >= 0.1) z = z * 100 / sizeOfStars; // _dockedSize = new GridLength(z, GridUnitType.Star); // if (_IsDockedInStack) _SizeDef = _dockedSize; // } //} ///// ///// If _IsDockedInStack, returns _SizeNow, else _dockedSize.Value. ///// //double _SizeNowDockedOrNot => _IsDockedInStack ? _SizeNow : _dockedSize.Value; ///// ///// Gets actual row height in vertical stack or column width in horizontal stack. ///// //double _SizeNow => new _RowCol(this).SizeNow; /// /// Gets or sets defined row height/unit in vertical stack or column width/unit in horizontal stack. /// GridLength _SizeDef { get => new _RowCol(this).SizeDef; set => new _RowCol(this) { SizeDef = value }; //set { // print.it(this, value.GridUnitType); // new _RowCol(this) { SizeDef = value }; //} } ///// ///// Gets GridLength with actual row height in vertical stack or column width in horizontal stack. ///// //GridLength _SizeDefNow => new _RowCol(this).SizeDefNow; /// /// Gets or sets minimal row height in vertical stack or column width in horizontal stack. /// double _SizeMin { //get => new _RowCol(this).SizeMin; set => new _RowCol(this) { SizeMin = value }; } ///// ///// Gets or sets maximal row height in vertical stack or column width in horizontal stack. ///// //double _SizeMax { // get => new _RowCol(this).SizeMax; // set => new _RowCol(this) { SizeMax = value }; //} /// /// Sets size = actual size and new unit. /// void _ChangeSizeUnit(GridUnitType unit, bool updateStars) { Debug.Assert(_IsDockedInStack); _dockedSize = new _RowCol(this).ChangeUnit(unit); if (updateStars && unit == GridUnitType.Star) //update other stars, else the splitter may jump foreach (var v in Parent._Stack_DockedNodes) if (v != this && v._dockedSize.IsStar) v._ChangeSizeUnit(unit, false); //unit is same, just update value in grid and _dockedSize } struct _RowCol { readonly DefinitionBase _d; public _RowCol(_Node node) { var stack = node.Parent._stack; int i = node._index * 2; if (stack.isVertical) _d = stack.grid.RowDefinitions[i]; else _d = stack.grid.ColumnDefinitions[i]; } public DefinitionBase RowCol => _d; public GridLength SizeDef { get => (_d is RowDefinition rd) ? rd.Height : (_d as ColumnDefinition).Width; set { if (_d is RowDefinition rd) rd.Height = value; else (_d as ColumnDefinition).Width = value; } } public double SizeNow => (_d is RowDefinition rd) ? rd.ActualHeight : (_d as ColumnDefinition).ActualWidth; public GridLength SizeDefNow => new GridLength(SizeNow, SizeDef.GridUnitType); public double SizeMin { get => (_d is RowDefinition rd) ? rd.MinHeight : (_d as ColumnDefinition).MinWidth; set { if (_d is RowDefinition rd) rd.MinHeight = value; else (_d as ColumnDefinition).MinWidth = value; } } public double SizeMax { get => (_d is RowDefinition rd) ? rd.MaxHeight : (_d as ColumnDefinition).MaxWidth; set { if (_d is RowDefinition rd) rd.MaxHeight = value; else (_d as ColumnDefinition).MaxWidth = value; } } public GridLength ChangeUnit(GridUnitType unit) { var r = new GridLength(SizeNow, unit); SizeDef = r; return r; } } static GridLength _GridLengthFromString(string s) { double w = 0; var u = GridUnitType.Star; if (s == null) u = GridUnitType.Auto; else if (s.Ends("*")) w = s.Length > 1 ? s.ToNumber(..^1) : 1.0; else { w = s.ToNumber(); u = GridUnitType.Pixel; } return new GridLength(w, u); //also tested GridLengthConverter } static string _GridLengthToString(GridLength k) { if (k.IsAuto) return null; var s = Math.Round(k.Value, 10).ToS(); return k.IsStar ? s + "*" : s; //GridLength.ToString is almost same, but: for Auto returns "Auto"; can return long string like "425.79999999999995" instead of "425.8". } #endregion } } ================================================ FILE: Au.Controls/KPanels/tab.cs ================================================ using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Input; using System.Windows.Interop; namespace Au.Controls; public partial class KPanels { partial class _Node { static readonly Style s_styleTabControl = XamlResources.Dictionary["AuPanelsTabControlStyle"] as Style, s_styleTabItem = XamlResources.Dictionary["AuPanelsTabItemStyle"] as Style, s_styleTabLeft = XamlResources.Dictionary["TabItemVerticalLeft"] as Style, s_tyleTabRight = XamlResources.Dictionary["TabItemVerticalRight"] as Style; void _InitTabControl() { var tc = _tab.tc; tc.Style = s_styleTabControl; if (_pm.HeaderBrush != Brushes.LightSteelBlue) { tc.ApplyTemplate(); if (VisualTreeHelper.GetChild(tc, 0) is Grid tg) tg.Background = _pm.HeaderBrush; //note: tc must have a parent. } tc.Padding = default; tc.TabStripPlacement = _headerAt; tc.SizeChanged += (_, e) => { switch (tc.TabStripPlacement) { case Dock.Top: case Dock.Bottom: return; } bool bigger = e.NewSize.Height > e.PreviousSize.Height; if (bigger != _tab.isVerticalHeader) _VerticalTabHeader(e.NewSize.Height); }; tc.ContextMenuOpening += _HeaderContextMenu; tc.PreviewMouseDown += _OnMouseDown; tc.SelectionChanged += (o, e) => { if (e.Source != o) return; //eg a descendant ComboBox e.Handled = true; if (e.AddedItems.Count == 0) return; var v = (e.AddedItems[0] as TabItem).Tag as _Node; v.TabSelected?.Invoke(v, EventArgs.Empty); }; //implement DontFocusTab (prevent changing focus when clicking a tabitem) tc.PreviewMouseLeftButtonDown += (_, e) => { if (e.Source is TabItem ti && ti.Tag is _Node n && n.DontFocusTab != null) { bool focusWithin; if (Keyboard.FocusedElement != null) { focusWithin = tc.IsKeyboardFocusWithin; } else { wnd w = Api.GetFocus(); if (!w.IsChildOf(ti.Hwnd())) return; focusWithin = null != tc.FindVisualDescendant(o => o is HwndHost hh && hh.Handle == w.Handle); } if (focusWithin && ti.IsSelected) return; ti.PreviewGotKeyboardFocus += _PreviewGotKeyboardFocus; ti.Dispatcher.InvokeAsync(() => { ti.PreviewGotKeyboardFocus -= _PreviewGotKeyboardFocus; }); void _PreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) { e.Handled = true; if (focusWithin) ti.Dispatcher.InvokeAsync(n.DontFocusTab); } //note: cannot implement it in _AddToTab, because then we don't know whether the tab header clicked or some descendant. } }; } void _VerticalTabHeader(double height = -1, bool onMove = false) { var tc = _tab.tc; if (tc.TabStripPlacement is Dock.Top or Dock.Bottom) return; if (height < 0) height = tc.ActualHeight; var d = _CalcHeight(); //not too slow bool vertHeader = d < height - 10; if (vertHeader == _tab.isVerticalHeader && !onMove) return; _tab.isVerticalHeader = vertHeader; var dock = tc.TabStripPlacement; foreach (TabItem v in tc.Items) { v.Style = vertHeader ? (dock == Dock.Left ? s_styleTabLeft : s_tyleTabRight) : s_styleTabItem; } double _CalcHeight() { var cult = CultureInfo.InvariantCulture; var fdir = tc.FlowDirection; var font = new Typeface(tc.FontFamily, tc.FontStyle, tc.FontWeight, tc.FontStretch); var fsize = tc.FontSize; var brush = SystemColors.ControlTextBrush; //var ppd = VisualTreeHelper.GetDpi(tc).PixelsPerDip; print.it(ppd); //ignored, and we don't need it double r = 4; foreach (TabItem v in tc.Items) { var f = new FormattedText(v.Header.ToString(), cult, fdir, font, fsize, brush, 1); r += f.Width + 11; } return r; } } /// /// Adds this to parent tab at startup or when moving. /// Caller before must call AddChild (or AddSibling) and set _index. /// void _AddToTab(bool moving) { var ti = new TabItem { Style = s_styleTabItem, Header = _leaf.name, Content = _elem, Tag = this }; var tc = Parent._tab.tc; tc.Items.Insert(_index, ti); if (moving) { _ShiftSiblingIndices(1); Parent._VerticalTabHeader(onMove: true); } } void _ShowHideInTab(bool show) { var tc = Parent._tab.tc; var ti = tc.Items[_index] as TabItem; if (!show) { var a = tc.Items.OfType().Where(o => o.Visibility == Visibility.Visible).ToArray(); if (a.Length > 1) { if (ti == tc.SelectedItem) { int i = Array.IndexOf(a, ti); if (++i == a.Length) i -= 2; tc.SelectedItem = a[i]; } } else if (!_IsDocument) { if (Parent._state == _DockState.Float) Parent._Hide(); else if (!Parent._state.Has(_DockState.Hide)) Parent._ShowHideInStack(show); } ti.Visibility = Visibility.Collapsed; ti.Content = null; } else { if (tc.Parent == null) { if (Parent._state.Has(_DockState.Float)) Parent._SetDockState(_DockState.Float); else Parent._ShowHideInStack(show); } ti.Content = _elem; ti.Visibility = Visibility.Visible; tc.SelectedItem = ti; } } void _ReorderInTab(_Node target, bool after) { if (target == this || (after && target.Next == this) || (!after && target.Previous == this)) return; Remove(); target.AddSibling(this, after); int index = 0; foreach (var v in Parent.Children()) v._index = index++; //to avoid auto-selecting next item when removed active item, we remove all inactive items and then add in new order. var tc = Parent._tab.tc; var sel = tc.SelectedItem; var a = tc.Items.OfType().ToArray(); for (int i = a.Length; --i >= 0;) if (a[i] != sel) tc.Items.RemoveAt(i); Array.Sort(a, (x, y) => (x.Tag as _Node)._index - (y.Tag as _Node)._index); for (int i = 0; i < a.Length; i++) if (a[i] != sel) tc.Items.Insert(i, a[i]); } static _Node _NodeFromTabItem(TabItem ti) => ti.Tag as _Node; } class _TabControl : TabControl { protected override void OnKeyDown(KeyEventArgs e) { //Apps often use Ctrl+Tab and Ctrl+Shift+Tab eg to switch documents, but TabControl would steal them for switching tabs. // To swith TabControl tabs also can be used Shift+Tab (makes tab item focused) then arrows. if (e.Key == Key.Tab && Keyboard.Modifiers is ModifierKeys.Control or (ModifierKeys.Control | ModifierKeys.Shift)) return; base.OnKeyDown(e); } } } ================================================ FILE: Au.Controls/KScintilla/KScintilla.cs ================================================ using System.Windows; using System.Windows.Interop; using System.Windows.Input; namespace Au.Controls; using static Sci; /// /// This .NET control wraps native Scintilla control. /// It is not a universal Scintilla wrapper class. Just for this library and related software. /// /// /// Most functions throw ArgumentOutOfRangeException when: 1. A position or line index argument is negative. 2. Scintilla returned a negative position or line index. /// If a position or line index argument is greater than text length or the number of lines, some functions return the text length or the last line, and it is documented; for other functions the behaviour is undefined, eg ArgumentOutOfRangeException or Scintilla's return value or like of the documented methods. /// /// Function/event names start with aa, because VS intellisense cannot group by inheritance and would mix with 300 WPF functions/events. /// public unsafe partial class KScintilla : HwndHost { wnd _w; WNDPROC _wndproc; nint _wndprocScintilla; nint _sciPtr; Sci_NotifyCallback _notifyCallback; int _managedThreadId; internal int _dpi; #if DEBUG public bool test_; //we use many scintilla controls, but often want to test something on one of them. Then set test_ = true... #endif static KScintilla() { Cpp.LoadAuNativeDll(SCINTILLA_DLL); } public KScintilla() { _adapter = new(this); } //public nint AaSciPtr => _sciPtr; public nint AaSciPtr { get { Debug.Assert(_sciPtr != 0); return _sciPtr; } } public SciImages AaImages { get; private set; } public SciTags AaTags => field ??= (AaInitTagsStyle == AaTagsStyle.NoTags ? null : new(this)); //can be used to add user-defined tags before creating window handle #region HwndHost public wnd AaWnd => _w; /// /// Invoked by AaOnHandleCreated, which is called by after initializing everything but before setting text and subclassing. /// public event Action AaHandleCreated; /// /// Called by after initializing everything but before setting text and subclassing. /// Invokes event . /// protected virtual void AaOnHandleCreated() => AaHandleCreated?.Invoke(this); protected override HandleRef BuildWindowCore(HandleRef hwndParent) { var wParent = (wnd)hwndParent.Handle; _dpi = Dpi.OfWindow(wParent); WS style = WS.CHILD; if (AaInitBorder) style |= WS.BORDER; //note: no WS_VISIBLE. WPF will manage it. It can cause visual artefacts occasionally, eg scrollbar in WPF area. _w = Api.CreateWindowEx(0, "Scintilla", Name, style, 0, 0, 0, 0, wParent); //size 0 0 is not the best, but it is a workaround for WPF bugs _sciPtr = _w.Send(SCI_GETDIRECTPOINTER); _managedThreadId = Environment.CurrentManagedThreadId; Call(SCI_SETNOTIFYCALLBACK, 0, Marshal.GetFunctionPointerForDelegate(_notifyCallback = _NotifyCallback)); bool hasTags = AaInitTagsStyle != AaTagsStyle.NoTags; if (AaInitReadOnlyAlways) { MOD mask = 0; if (AaInitImages || hasTags) mask |= MOD.SC_MOD_INSERTTEXT | MOD.SC_MOD_DELETETEXT; Call(SCI_SETMODEVENTMASK, (int)mask); } _InitDocument(); Call(SCI_SETSCROLLWIDTHTRACKING, 1); Call(SCI_SETSCROLLWIDTH, 1); //TODO3: later make narrower when need, eg when folded long lines (alas there is no direct notification). Maybe use timer. if (!AaInitUseDefaultContextMenu) Call(SCI_USEPOPUP); Call(SCI_SETCARETWIDTH, 2); //not DPI-scaled //Need to set selection colors or layer, because the default inactive selection color is darker than active. // It is 0x3F808080, but alpha is ignored if SC_LAYER_BASE (default). Call(SCI_SETSELECTIONLAYER, SC_LAYER_UNDER_TEXT); aaaSetElementColor(SC_ELEMENT_SELECTION_BACK, 0xA0A0A0A0); //use alpha to mix with indicators aaaSetElementColor(SC_ELEMENT_SELECTION_INACTIVE_BACK, 0x60A0A0A0); aaaSetElementColor(SC_ELEMENT_SELECTION_ADDITIONAL_BACK, 0x60A0A0A0); if (AaInitWrapVisuals) { Call(SCI_SETWRAPVISUALFLAGS, SC_WRAPVISUALFLAG_START | SC_WRAPVISUALFLAG_END); Call(SCI_SETWRAPVISUALFLAGSLOCATION, SC_WRAPVISUALFLAGLOC_END_BY_TEXT); Call(SCI_SETWRAPINDENTMODE, SC_WRAPINDENT_SAME); } if (AaWrapLines) { Call(SCI_SETWRAPMODE, SC_WRAP_WORD); } //note: cannot set styles here, because later derived class will call aaaStyleClearAll, which sets some special styles. if (AaInitImages) AaImages = new SciImages(this); if (FocusManager.GetFocusScope(this) is Window fs && FocusManager.GetFocusedElement(fs) == this && Api.GetFocus() == wParent) Api.SetFocus(_w); AaOnHandleCreated(); _adapter.HandleCreated(); //after derived classes set styles etc _wndprocScintilla = Api.SetWindowLongPtr(_w, GWL.WNDPROC, Marshal.GetFunctionPointerForDelegate(_wndproc = _WndProc)); //WPF will subclass this window. It respects the GWL.WNDPROC subclass, but breaks SetWindowSubclass. return new HandleRef(this, _w.Handle); } void _InitDocument() { //these must be set for each document of this Scintilla window Call(SCI_SETCODEPAGE, Api.CP_UTF8); Call(SCI_SETTABWIDTH, 4); if (AaInitReadOnlyAlways) { Call(SCI_SETREADONLY, 1); Call(SCI_SETUNDOCOLLECTION); } //else if (_isReadOnly) Call(SCI_SETREADONLY, 1); } protected override void DestroyWindowCore(HandleRef hwnd) { WndUtil.DestroyWindow((wnd)hwnd.Handle); _w = default; _sciPtr = 0; _acc?.Dispose(); _acc = null; //workaround for: never GC-collected if disposed before removing from parent WPF element (shouldn't do it). if (this is IKeyboardInputSink iks) { Debug_.PrintIf(iks.KeyboardInputSite != null); iks.KeyboardInputSite?.Unregister(); } //GC.ReRegisterForFinalize(this); //to detect memory leak } //~KScintilla() { print.it("~KScintilla"); } //to detect memory leak. Also enable the GC.ReRegisterForFinalize. //static PrintMsgOptions s_pmo = new(Api.WM_TIMER, Api.WM_MOUSEMOVE, Api.WM_SETCURSOR, Api.WM_NCHITTEST, Api.WM_PAINT, Api.WM_IME_SETCONTEXT, Api.WM_IME_NOTIFY); nint _WndProc(wnd w, int msg, nint wp, nint lp) { //if (Name == "x") WndUtil.PrintMsg(w, msg, wp, lp); //if(Name == "x") WndUtil.PrintMsg(_w, msg, wp, lp, s_pmo); switch (msg) { case Api.WM_SETFOCUS: if (!_inOnWmSetFocus) if (_OnWmSetFocus()) return 0; break; case Api.WM_KILLFOCUS: if (_inOnWmSetFocus) return 0; break; case Api.WM_LBUTTONDOWN or Api.WM_RBUTTONDOWN or Api.WM_MBUTTONDOWN: if (Api.GetFocus() != _w) { bool setFocus = !AaNoMouseSetFocus.Has(_MouseButton(msg)); if (setFocus && msg == Api.WM_LBUTTONDOWN && AaInitReadOnlyAlways) { //don't focus if link clicked int pos = Call(SCI_CHARPOSITIONFROMPOINTCLOSE, Math2.LoShort(lp), Math2.HiShort(lp)); if (pos >= 0) { if (aaaStyleHotspot(aaaStyleGetAt(pos))) setFocus = false; else { //indicator-link? uint indic = (uint)Call(SCI_INDICATORALLONFOR, pos); for (int i = 0; indic != 0; i++, indic >>>= 1) if (0 != (indic & 1) && 0 != Call(SCI_INDICGETHOVERFORE, i)) setFocus = false; } } } if (setFocus) this.Focus(); } break; case Api.WM_LBUTTONUP: if (AaInitReadOnlyAlways && Api.GetFocus() != _w) if (aaaHasSelection) this.Focus(); break; } static MButtons _MouseButton(int msg) => msg switch { Api.WM_LBUTTONDOWN or Api.WM_LBUTTONUP => MButtons.Left, Api.WM_RBUTTONDOWN or Api.WM_RBUTTONUP => MButtons.Right, Api.WM_MBUTTONDOWN or Api.WM_MBUTTONUP => MButtons.Middle, _ => 0 }; var R = WndProc(w, msg, wp, lp); switch (msg) { case Api.WM_TIMER when wp == 5: //Workaround for Scintilla bug: bad scrollbar pos on "open file and go to line". // Scintilla adds scrollbars after ~400 ms. For it uses this timer. Api.SCROLLINFO x = new(Api.SIF_POS); if (x.Get(w, true)) { int line = Call(Sci.SCI_GETFIRSTVISIBLELINE); if (x.nPos != line) { x.nPos = line; x.Set(w, true); } } break; } return R; } protected virtual nint WndProc(wnd w, int msg, nint wp, nint lp) { //return CallRetPtr(msg, wp, lp); //no, then Scintilla does not process WM_NCDESTROY return Api.CallWindowProc(_wndprocScintilla, w, msg, wp, lp); } protected override IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { if (msg == Api.WM_GETOBJECT) { //WPF steals it from _WndProc handled = true; return (_acc ??= new _Accessible(this)).WmGetobject(wParam, lParam); } return base.WndProc(hwnd, msg, wParam, lParam, ref handled); } protected override void OnDpiChanged(DpiScale oldDpi, DpiScale newDpi) { if (!_w.Is0 && newDpi.PixelsPerDip != oldDpi.PixelsPerDip) { _dpi = newDpi.PixelsPerInchY.ToInt(); _MarginWidthsDpiChanged(); } base.OnDpiChanged(oldDpi, newDpi); } #region problems with focus, keyboard, destroying //Somehow WPF does not care about native control focus, normal keyboard work, destroying, etc. //1. No Tab key navigation. Also does not set focus when parent tab item selected. // Workaround: override TabIntoCore and call API SetFocus. //2. Does not set logical focus to HwndHost when its native control is really focused. Then eg does not restore real focus after using menu. // Workaround: set focus on WM_LBUTTONDOWN etc. Also on WM_SETFOCUS, with some tricks. //3. Steals arrow keys, Tab and Enter from native control and sets focus to other controls or closes dialog. // Workaround: override TranslateAcceleratorCore, pass the keys to the control and return true. //4. When closing parent window, does not destroy hwnhosted controls. Instead moves to a hidden parking window, and destroys later on GC if you are careful. // Need to always test whether hwnhosted controls are destroyed on GC, to avoid leaked windows + many managed objects. // Eg to protect wndproc delegate from GC don't add it to a thread-static array until destroyed; let it be a field of the wrapper class. // Or let app dispose the HwndHost in OnClosing. But control itself cannot reliably know when to self-destroy. //5. When closing parent window, briefly tries to show native control, and focus if was focused. // Workaround: let app dispose the HwndHost in OnClosing. //Never mind: after SetFocus, Keyboard.FocusedElement is null. bool _OnWmSetFocus() { //keep logical focus on HwndHost, else will not work eg restoring of real focus when closing menu. if (IsVisible && Focusable) { //info: !IsVisible when closing window without disposing this (WPF bug) var fs = FocusManager.GetFocusScope(this); if (fs != null && FocusManager.GetFocusedElement(fs) != this) { //focused not by WPF _inOnWmSetFocus = true; FocusManager.SetFocusedElement(fs, this); //in some cases would work better than this.Focus() _inOnWmSetFocus = false; //all WPF 'Focus' functions make the main window focused. Then OnGotKeyboardFocus makes _w focused again. // Wndproc receives wm_setfocus, wm_killfocus and wm_setfocus. Passes to scintilla only the last wm_setfocus. // Can prevent this, eg set IsFocusable=false before, but then WPF in some cases does not restore focus after switching windows etc. // To prevent this on click, Wndproc calls Focus, and scintilla does not call SetFocus (mod). // Could not find a way to avoid this in other cases, never mind. return true; } } return false; } bool _inOnWmSetFocus; //Makes _w focused when called this.Focus() or Keyboard.Focus(this). protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e) { e.Handled = true; Api.SetFocus(_w); base.OnGotKeyboardFocus(e); } //Sets focus when tabbed to this or when clicked the parent tab item. Like eg WPF TextBox. protected override bool TabIntoCore(TraversalRequest request) { Focus(); return true; //base.TabIntoCore(request); //empty func, returns false } protected override bool TranslateAcceleratorCore(ref System.Windows.Interop.MSG msg, ModifierKeys modifiers) { var m = msg.message; var k = (KKey)msg.wParam; //if (m == Api.WM_KEYDOWN) print.it(m, k); if (m is Api.WM_KEYDOWN or Api.WM_KEYUP /*or Api.WM_SYSKEYDOWN or Api.WM_SYSKEYUP*/) if (!modifiers.Has(ModifierKeys.Alt)) { switch (k) { case KKey.Left or KKey.Right or KKey.Up or KKey.Down: case KKey.Enter when modifiers == 0 && !aaaIsReadonly: case KKey.Tab when !modifiers.Has(ModifierKeys.Control) && !aaaIsReadonly: Call(msg.message, msg.wParam, msg.lParam); //not DispatchMessage or Send return true; case KKey.Insert when modifiers == 0: return true; } } return base.TranslateAcceleratorCore(ref msg, modifiers); } //Without this, user cannot type eg character 'a' in HwndHost'ed control if there is button with text like "_Apply". protected override bool TranslateCharCore(ref System.Windows.Interop.MSG msg, ModifierKeys modifiers) { if (msg.message is not (Api.WM_CHAR or Api.WM_DEADCHAR)) return false; //WM_SYSCHAR etc if with Alt if (msg.hwnd != _w.Handle) return false; //WPF bug. Eg when on key down the app makes this control focused. if ((int)msg.wParam <= 32) return false; //eg control chars on Ctrl+key _w.Send(msg.message, msg.wParam, msg.lParam); //not Call or WndProc return true; } #endregion #endregion void _NotifyCallback(void* cbParam, ref SCNotification n) { try { var code = n.code; //if(code != NOTIF.SCN_PAINTED) print.qm2.write(code.ToString()); switch (code) { case NOTIF.SCN_MODIFIED: var mt = n.modificationType; //if(this.Name!= "Output_text") print.it(mt, n.position); if (mt.HasAny(MOD.SC_MOD_INSERTTEXT | MOD.SC_MOD_DELETETEXT)) { _adapter.TextModified(); bool inserted = mt.Has(MOD.SC_MOD_INSERTTEXT); _RdOnModified(inserted, n); //AaImages?.OnTextChanged_(inserted, n); AaTags?.OnTextChanged_(inserted, n); } else if (n.modificationType.Has(Sci.MOD.SC_MOD_BEFOREDELETE)) { _DeleteMarkers(ref n); } //if(mt.Has(MOD.SC_MOD_CHANGEANNOTATION)) ChangedAnnotation?.Invoke(this, ref n); if (AaDisableModifiedNotifications) return; break; case NOTIF.SCN_HOTSPOTRELEASECLICK: if (aaaHasSelection) return; AaTags?.OnLinkClick_(n.position, 0 != (n.modifiers & SCMOD_CTRL)); break; case NOTIF.SCN_INDICATORRELEASE: if (aaaHasSelection) return; break; } AaOnSciNotify(ref n); } catch (Exception e1) when (!Debugger.IsAttached) { //DispatcherUnhandledException not raised on exception here. Let's add handler code here like in App._Main. if (1 != dialog.showError("Exception", e1.ToStringWithoutStack(), "1 Continue|2 Exit", DFlags.Wider, _w.Window, e1.ToString())) Environment.Exit(1); } } /// /// Raises the event. /// protected virtual void AaOnSciNotify(ref SCNotification n) { AaNotify?.Invoke(new(this, ref n)); switch (n.code) { case NOTIF.SCN_MODIFIED: if (AaTextChanged is { } e && n.modificationType.HasAny(MOD.SC_MOD_INSERTTEXT | MOD.SC_MOD_DELETETEXT)) e(new(this, ref n)); break; } } public ref struct AaEventHandlerArgs { public readonly KScintilla c; public readonly ref SCNotification n; public AaEventHandlerArgs(KScintilla sci, ref SCNotification notif) { c = sci; n = ref notif; } } public delegate void AaEventHandler(AaEventHandlerArgs e); /// /// Occurs when any Scintilla notification is received. /// public event AaEventHandler AaNotify; /// /// Occurs when text changed (SCN_MODIFIED notification with SC_MOD_INSERTTEXT or SC_MOD_DELETETEXT). /// public event AaEventHandler AaTextChanged; //workaround for: Scintilla does not delete markers of deleted lines. Instead moves to the next or previous line. void _DeleteMarkers(ref SCNotification n) { int end = n.position + n.length; int line1 = aaaLineFromPos(false, n.position), line2 = aaaLineFromPos(false, end); if (end == aaaLen8) line2++; if (line2 <= line1) return; int start1 = aaaLineStart(false, line1); if (start1 < n.position) line1++; for (int line = line1; line < line2; line++) { uint markers = (uint)Call(Sci.SCI_MARKERGET, line); //never mind: no folding markers if (markers != 0) AaOnDeletingLineWithMarkers(line, markers); } } /// /// Called before deleting a line that contains markers (except folding markers), unless MOD.SC_MOD_BEFOREDELETE notification removed with SCI_SETMODEVENTMASK. /// Deletes all these markers; an override can prevent it by not calling the base method. /// /// /// See SCI_MARKERGET. protected virtual void AaOnDeletingLineWithMarkers(int line, uint markers) { for (uint i = 0, m = markers; m != 0; m >>= 1, i++) { if ((m & 1) != 0) Call(SCI_MARKERDELETE, line, (int)i); } } /// /// Sends a Scintilla message to the control and returns int. /// Don't call this function from another thread. /// [DebuggerStepThrough] public int Call(int sciMessage, nint wParam = 0, nint lParam = 0) => (int)CallRetPtr(sciMessage, wParam, lParam); /// /// Sends a Scintilla message to the control and returns int. /// Don't call this function from another thread. /// [DebuggerStepThrough] public int Call(int sciMessage, nint wParam, void* lParam) => (int)CallRetPtr(sciMessage, wParam, (nint)lParam); /// /// Sends a Scintilla message to the control and returns int. /// Don't call this function from another thread. /// [DebuggerStepThrough] public int Call(int sciMessage, nint wParam, bool lParam) => (int)CallRetPtr(sciMessage, wParam, lParam ? 1 : 0); /// /// Sends a Scintilla message to the control and returns int. /// Don't call this function from another thread. /// [DebuggerStepThrough] public int Call(int sciMessage, bool wParam, nint lParam = 0) => (int)CallRetPtr(sciMessage, wParam ? 1 : 0, lParam); /// /// Sends a Scintilla message to the control and returns nint. /// Don't call this function from another thread. /// [DebuggerStepThrough] public nint CallRetPtr(int sciMessage, nint wParam = 0, nint lParam = 0) { #if DEBUG if (AaDebugPrintMessages_) _DebugPrintMessage(sciMessage); Debug.Assert(_sciPtr != 0); //0 before creating or after destroying Scintilla window. //note: don't auto-create handle. It can be dangerous, create parked control, etc. Debug.Assert(Environment.CurrentManagedThreadId == _managedThreadId); //possible wrong thread eg if an async continuation cannot be executed in correct thread, // probably because there is no WPF SynchronizationContext, eg Application.Run ended on exception. #else if (_sciPtr == 0) throw new InvalidOperationException("KScintilla.CallRetPtr: _sciPtr==null"); if (Environment.CurrentManagedThreadId != _managedThreadId) throw new InvalidOperationException("KScintilla.CallRetPtr: wrong thread"); #endif return Sci_Call(_sciPtr, sciMessage, wParam, lParam); } #if DEBUG static void _DebugPrintMessage(int sciMessage) { if (sciMessage < SCI_START) return; switch (sciMessage) { case SCI_COUNTCODEUNITS: case SCI_POSITIONRELATIVECODEUNITS: case SCI_CANUNDO: case SCI_CANREDO: case SCI_GETREADONLY: case SCI_GETSELECTIONEMPTY: //case SCI_GETTEXTLENGTH: return; } if (s_debugPM == null) { s_debugPM = new(); foreach (var v in typeof(Sci).GetFields()) { var s = v.Name; //print.it(v.Name); if (s.Starts("SCI_")) s_debugPM.Add((int)v.GetRawConstantValue(), s); } } if (!s_debugPM.TryGetValue(sciMessage, out var k)) { k = sciMessage.ToString(); } print.qm2.write(k); } static Dictionary s_debugPM; internal bool AaDebugPrintMessages_ { get; set; } #endif #region properties /// /// Border style. /// Must be set before creating control handle. /// public bool AaInitBorder { get; set; } /// /// Use the default Scintilla's context menu. /// Must be set before creating control handle. /// public bool AaInitUseDefaultContextMenu { get; set; } /// /// This control is used just to display text, not to edit. /// Must be set before creating control handle. /// public bool AaInitReadOnlyAlways { get; set; } /// /// Whether to show images specified in tags like <image "image file path">, including icons of non-image file types. /// Must be set before creating control handle. /// If false, property is null. /// public bool AaInitImages { get; set; } /// /// See . /// public enum AaTagsStyle { /// Don't support tags. The property is null. NoTags, /// Let , aaaSetText and aaaAppendText parse tags when the text has prefix "<>". AutoWithPrefix, /// Let , aaaSetText and aaaAppendText parse tags always. AutoAlways, /// Tags are parsed only when calling Tags.AddText. User, } /// /// Whether and when supports tags. /// Must be set before creating control handle. /// public AaTagsStyle AaInitTagsStyle { get; set; } /// /// Whether to show arrows etc to make wrapped lines more visible. /// Must be set before creating control handle. /// public bool AaInitWrapVisuals { get; set; } = true; /// /// Word-wrap. /// public bool AaWrapLines { get => _wrapLines; set { if (value != _wrapLines) { _wrapLines = value; if (!_w.Is0) Call(SCI_SETWRAPMODE, value ? SC_WRAP_WORD : 0); } } } bool _wrapLines; /// /// Whether uses Enter key. /// If null (default), false if is true. /// public bool? AaUsesEnter { get; set; } /// /// On SCN_MODIFIED notifications suppress , and . /// Use to temporarily disable 'modified' notifications. Never use SCI_SETMODEVENTMASK, because then the control would stop working correctly. /// public bool AaDisableModifiedNotifications { get; set; } /// /// Don't set focus on mouse left/right/middle button down. /// public MButtons AaNoMouseSetFocus { get; set; } #endregion #region range data struct _RangeData { public int from, to; public object data; } List<_RangeData> _rd; bool _rdLocked; /// /// Attaches any data to a range of text. Like a hidden indicator with attached data of any type. /// /// Called from . public void AaRangeDataAdd(bool utf16, Range r, object data) { if (_rdLocked) throw new InvalidOperationException("Called from event handler."); var (from, to) = aaaNormalizeRange(utf16, r); _rd ??= new(); _rd.Add(new() { from = from, to = to, data = data }); } /// /// Gets data of type T attached to a range of text with at the specified position. /// /// Receives data. Use type object to get data of any type. /// true if pos is in a range added with and the range data type is T (or inherited). /// Called from . public bool AaRangeDataGet(bool utf16, int pos, out T data) where T : class => AaRangeDataGet(utf16, pos, out data, out _, out _); /// /// Gets data of type T attached to a range of text with at the specified position. /// /// Receives data. Use type object to get data of any type. /// true if pos is in a range added with and the range data type is T (or inherited). /// Called from . public bool AaRangeDataGet(bool utf16, int pos, out T data, out int from, out int to) where T : class { if (_rdLocked) throw new InvalidOperationException("Called from AaRangeDataRemoved."); pos = _ParamPos(utf16, pos); foreach (ref var v in _rd.AsSpan()) { if (pos >= v.from && pos < v.to && v.data is T d) { data = d; from = v.from; to = v.to; return true; } } data = null; from = 0; to = 0; return false; } /// /// Gets all datas of type T added with . /// /// public IEnumerable AaRangeDataEnum() { if (!_rd.NE_()) { foreach (var v in _rd) if (v.data is T r) yield return r; } } /// /// When a text range registered with removed (when control text changed). /// /// /// The event handler must not modify control text and must not call AaRangeDataX functions. /// public event Action AaRangeDataRemoved; void _RdOnModified(bool inserted, in SCNotification n) { if (_rd.NE_()) return; if (_rdLocked) throw new InvalidOperationException("Called from event handler."); _rdLocked = true; try { int start = n.position, end = start + n.length, len = n.length; if (inserted) { foreach (ref var v in _rd.AsSpan()) { if (start < v.to) { if (start <= v.from) v.from += len; v.to += len; } } } else { if (start == 0 && aaaLen8 == 0) { //deleted all text if (AaRangeDataRemoved != null) foreach (var v in _rd) AaRangeDataRemoved(v.data); _rd.Clear(); return; } System.Collections.BitArray remove = null; int j = -1; foreach (ref var v in _rd.AsSpan()) { j++; if (start < v.to) { if (end < v.from) { v.from -= len; v.to -= len; } else if (start <= v.from) { if (end < v.to) { v.to -= len; v.from = start; } else { remove ??= new(_rd.Count); remove[j] = true; AaRangeDataRemoved?.Invoke(v.data); } } else if (end < v.to) { v.to -= len; } else { v.to = start; } } } if (remove != null) { for (int i = remove.Count; --i >= 0;) { if (remove[i]) _rd.RemoveAt(i); } } } } finally { _rdLocked = false; } //print.it("ranges", _rd.Select(o => (o.from, o.to, o.data))); } #endregion #region acc _Accessible _acc; class _Accessible : HwndHostAccessibleBase_ { readonly KScintilla _sci; internal _Accessible(KScintilla sci) : base(sci, sci.AaWnd) { _sci = sci; } public override ERole Role(int child) => _sci.AaAccessibleRole; public override string Name(int child) => _sci.AaAccessibleName; public override string Description(int child) => _sci.AaAccessibleDescription; public override string Value(int child) => _sci.AaAccessibleValue; public override EState State(int child) { var r = base.State(child); if (_sci.aaaIsReadonly) r |= EState.READONLY; return r; } } protected virtual ERole AaAccessibleRole => ERole.TEXT; protected virtual string AaAccessibleName => Name; protected virtual string AaAccessibleDescription => null; protected virtual string AaAccessibleValue => AaInitReadOnlyAlways ? aaaText?.Limit(0xffff) : null; #endregion } ================================================ FILE: Au.Controls/KScintilla/Sci API.cs ================================================ namespace Au.Controls; public static unsafe class Sci { #region au modifications public const int SCI_MARGINSTYLENEXT = 9502; public delegate void Sci_NotifyCallback(void* cbParam, ref SCNotification n); public const int SCI_SETNOTIFYCALLBACK = 9503; public delegate int Sci_AnnotationDrawCallback(void* cbParam, ref Sci_AnnotationDrawCallbackData d); public const int SCI_SETANNOTATIONDRAWCALLBACK = 9504; public delegate void Sci_MarginDrawCallback(ref Sci_MarginDrawCallbackData d); public const int SCI_SETMARGINDRAWCALLBACK = 9505; public const int SCI_ISXINMARGIN = 9506; public const int SCI_DRAGDROP = 9507; public const string SCINTILLA_DLL = "Scintilla.dll"; [DllImport(SCINTILLA_DLL, EntryPoint = "Scintilla_DirectFunction")] public static extern nint Sci_Call(nint sci, int message, nint wParam = 0, nint lParam = 0); [DllImport(SCINTILLA_DLL)] public static extern int Sci_Range(nint sci, int start8, int end8, out byte* p1, out byte* p2, int* length = null); [DllImport(SCINTILLA_DLL)] public static extern void Sci_SetFoldLevels(nint sci, int line, int lastLine, int len, int* a); public record struct Sci_VisibleRange { public int dlineFrom, dlineTo, vlineFrom, vlineTo, posFrom, posTo; } /// /// flags: 1 need pos /// [DllImport(SCINTILLA_DLL)] public static extern void Sci_GetVisibleRange(nint sci, out Sci_VisibleRange r); //[DllImport("Lexilla.dll", EntryPoint = "CreateLexer")] //public static extern nint Sci_CreateLexer(byte[] lexer); #pragma warning disable 649 public unsafe struct Sci_AnnotationDrawCallbackData { public int step; public IntPtr hdc; public RECT rect; public byte* text; public int textLen, line, annotLine; }; public unsafe struct Sci_MarginDrawCallbackData { public IntPtr hdc; public RECT rect; public int margin, firstLine, lastLine; }; public struct Sci_DragDropData { public int x, y; public byte* text; public int len; public int copy; //bool } #pragma warning restore 649 public const int STYLE_HIDDEN = 31; //DEFAULT-1 #endregion public delegate nint SciFnDirectStatus(nint ptr, int iMessage, nint wParam, nint lParam, out int pStatus); public const int INVALID_POSITION = -1; public const int SCI_START = 2000; public const int SCI_OPTIONAL_START = 3000; public const int SCI_LEXER_START = 4000; public const int SCI_ADDTEXT = 2001; public const int SCI_ADDSTYLEDTEXT = 2002; public const int SCI_INSERTTEXT = 2003; public const int SCI_CHANGEINSERTION = 2672; public const int SCI_CLEARALL = 2004; public const int SCI_DELETERANGE = 2645; public const int SCI_CLEARDOCUMENTSTYLE = 2005; public const int SCI_GETLENGTH = 2006; public const int SCI_GETCHARAT = 2007; public const int SCI_GETCURRENTPOS = 2008; public const int SCI_GETANCHOR = 2009; //public const int SCI_GETSTYLEAT = 2010; //obsolete, use SCI_GETSTYLEINDEXAT public const int SCI_GETSTYLEINDEXAT = 2038; public const int SCI_REDO = 2011; public const int SCI_SETUNDOCOLLECTION = 2012; public const int SCI_SELECTALL = 2013; public const int SCI_SETSAVEPOINT = 2014; public const int SCI_GETSTYLEDTEXT = 2015; public const int SCI_GETSTYLEDTEXTFULL = 2778; public const int SCI_CANREDO = 2016; public const int SCI_MARKERLINEFROMHANDLE = 2017; public const int SCI_MARKERDELETEHANDLE = 2018; public const int SCI_MARKERHANDLEFROMLINE = 2732; public const int SCI_MARKERNUMBERFROMLINE = 2733; public const int SCI_GETUNDOCOLLECTION = 2019; public const int SCWS_INVISIBLE = 0; public const int SCWS_VISIBLEALWAYS = 1; public const int SCWS_VISIBLEAFTERINDENT = 2; public const int SCWS_VISIBLEONLYININDENT = 3; public const int SCI_GETVIEWWS = 2020; public const int SCI_SETVIEWWS = 2021; public const int SCTD_LONGARROW = 0; public const int SCTD_STRIKEOUT = 1; public const int SCI_GETTABDRAWMODE = 2698; public const int SCI_SETTABDRAWMODE = 2699; public const int SCI_POSITIONFROMPOINT = 2022; public const int SCI_POSITIONFROMPOINTCLOSE = 2023; public const int SCI_GOTOLINE = 2024; public const int SCI_GOTOPOS = 2025; public const int SCI_SETANCHOR = 2026; public const int SCI_GETCURLINE = 2027; public const int SCI_GETENDSTYLED = 2028; public const int SC_EOL_CRLF = 0; public const int SC_EOL_CR = 1; public const int SC_EOL_LF = 2; public const int SCI_CONVERTEOLS = 2029; public const int SCI_GETEOLMODE = 2030; public const int SCI_SETEOLMODE = 2031; public const int SCI_STARTSTYLING = 2032; public const int SCI_SETSTYLING = 2033; public const int SCI_GETBUFFEREDDRAW = 2034; public const int SCI_SETBUFFEREDDRAW = 2035; public const int SCI_SETTABWIDTH = 2036; public const int SCI_GETTABWIDTH = 2121; public const int SCI_CLEARTABSTOPS = 2675; public const int SCI_ADDTABSTOP = 2676; public const int SCI_GETNEXTTABSTOP = 2677; public const int SC_CP_UTF8 = 65001; public const int SCI_SETCODEPAGE = 2037; public const int SCI_SETFONTLOCALE = 2760; public const int SCI_GETFONTLOCALE = 2761; public const int SC_IME_WINDOWED = 0; public const int SC_IME_INLINE = 1; public const int SCI_GETIMEINTERACTION = 2678; public const int SCI_SETIMEINTERACTION = 2679; public const int MARKER_MAX = 31; public const int SC_MARK_CIRCLE = 0; public const int SC_MARK_ROUNDRECT = 1; public const int SC_MARK_ARROW = 2; public const int SC_MARK_SMALLRECT = 3; public const int SC_MARK_SHORTARROW = 4; public const int SC_MARK_EMPTY = 5; public const int SC_MARK_ARROWDOWN = 6; public const int SC_MARK_MINUS = 7; public const int SC_MARK_PLUS = 8; public const int SC_MARK_VLINE = 9; public const int SC_MARK_LCORNER = 10; public const int SC_MARK_TCORNER = 11; public const int SC_MARK_BOXPLUS = 12; public const int SC_MARK_BOXPLUSCONNECTED = 13; public const int SC_MARK_BOXMINUS = 14; public const int SC_MARK_BOXMINUSCONNECTED = 15; public const int SC_MARK_LCORNERCURVE = 16; public const int SC_MARK_TCORNERCURVE = 17; public const int SC_MARK_CIRCLEPLUS = 18; public const int SC_MARK_CIRCLEPLUSCONNECTED = 19; public const int SC_MARK_CIRCLEMINUS = 20; public const int SC_MARK_CIRCLEMINUSCONNECTED = 21; public const int SC_MARK_BACKGROUND = 22; public const int SC_MARK_DOTDOTDOT = 23; public const int SC_MARK_ARROWS = 24; public const int SC_MARK_PIXMAP = 25; public const int SC_MARK_FULLRECT = 26; public const int SC_MARK_LEFTRECT = 27; public const int SC_MARK_AVAILABLE = 28; public const int SC_MARK_UNDERLINE = 29; public const int SC_MARK_RGBAIMAGE = 30; public const int SC_MARK_BOOKMARK = 31; public const int SC_MARK_VERTICALBOOKMARK = 32; public const int SC_MARK_BAR = 33; public const int SC_MARK_CHARACTER = 10000; public const int SC_MARKNUM_HISTORY_REVERTED_TO_ORIGIN = 21; public const int SC_MARKNUM_HISTORY_SAVED = 22; public const int SC_MARKNUM_HISTORY_MODIFIED = 23; public const int SC_MARKNUM_HISTORY_REVERTED_TO_MODIFIED = 24; public const int SC_MARKNUM_FOLDEREND = 25; public const int SC_MARKNUM_FOLDEROPENMID = 26; public const int SC_MARKNUM_FOLDERMIDTAIL = 27; public const int SC_MARKNUM_FOLDERTAIL = 28; public const int SC_MARKNUM_FOLDERSUB = 29; public const int SC_MARKNUM_FOLDER = 30; public const int SC_MARKNUM_FOLDEROPEN = 31; public const int SC_MASK_HISTORY = 0x01E00000; public const int SC_MASK_FOLDERS = unchecked((int)0xFE000000); public const int SCI_MARKERDEFINE = 2040; public const int SCI_MARKERSETFORE = 2041; public const int SCI_MARKERSETBACK = 2042; public const int SCI_MARKERSETBACKSELECTED = 2292; public const int SCI_MARKERSETFORETRANSLUCENT = 2294; public const int SCI_MARKERSETBACKTRANSLUCENT = 2295; public const int SCI_MARKERSETBACKSELECTEDTRANSLUCENT = 2296; public const int SCI_MARKERSETSTROKEWIDTH = 2297; public const int SCI_MARKERENABLEHIGHLIGHT = 2293; public const int SCI_MARKERADD = 2043; public const int SCI_MARKERDELETE = 2044; public const int SCI_MARKERDELETEALL = 2045; public const int SCI_MARKERGET = 2046; public const int SCI_MARKERNEXT = 2047; public const int SCI_MARKERPREVIOUS = 2048; public const int SCI_MARKERDEFINEPIXMAP = 2049; public const int SCI_MARKERADDSET = 2466; public const int SCI_MARKERSETALPHA = 2476; public const int SCI_MARKERGETLAYER = 2734; public const int SCI_MARKERSETLAYER = 2735; public const int SC_MAX_MARGIN = 4; public const int SC_MARGIN_SYMBOL = 0; public const int SC_MARGIN_NUMBER = 1; public const int SC_MARGIN_BACK = 2; public const int SC_MARGIN_FORE = 3; public const int SC_MARGIN_TEXT = 4; public const int SC_MARGIN_RTEXT = 5; public const int SC_MARGIN_COLOUR = 6; public const int SCI_SETMARGINTYPEN = 2240; public const int SCI_GETMARGINTYPEN = 2241; public const int SCI_SETMARGINWIDTHN = 2242; public const int SCI_GETMARGINWIDTHN = 2243; public const int SCI_SETMARGINMASKN = 2244; public const int SCI_GETMARGINMASKN = 2245; public const int SCI_SETMARGINSENSITIVEN = 2246; public const int SCI_GETMARGINSENSITIVEN = 2247; public const int SCI_SETMARGINCURSORN = 2248; public const int SCI_GETMARGINCURSORN = 2249; public const int SCI_SETMARGINBACKN = 2250; public const int SCI_GETMARGINBACKN = 2251; public const int SCI_SETMARGINS = 2252; public const int SCI_GETMARGINS = 2253; public const int STYLE_DEFAULT = 32; public const int STYLE_LINENUMBER = 33; public const int STYLE_BRACELIGHT = 34; public const int STYLE_BRACEBAD = 35; public const int STYLE_CONTROLCHAR = 36; public const int STYLE_INDENTGUIDE = 37; public const int STYLE_CALLTIP = 38; public const int STYLE_FOLDDISPLAYTEXT = 39; public const int STYLE_LASTPREDEFINED = 39; public const int STYLE_MAX = 255; public const int SC_CHARSET_ANSI = 0; public const int SC_CHARSET_DEFAULT = 1; public const int SC_CHARSET_BALTIC = 186; public const int SC_CHARSET_CHINESEBIG5 = 136; public const int SC_CHARSET_EASTEUROPE = 238; public const int SC_CHARSET_GB2312 = 134; public const int SC_CHARSET_GREEK = 161; public const int SC_CHARSET_HANGUL = 129; public const int SC_CHARSET_MAC = 77; public const int SC_CHARSET_OEM = 255; public const int SC_CHARSET_RUSSIAN = 204; public const int SC_CHARSET_OEM866 = 866; public const int SC_CHARSET_CYRILLIC = 1251; public const int SC_CHARSET_SHIFTJIS = 128; public const int SC_CHARSET_SYMBOL = 2; public const int SC_CHARSET_TURKISH = 162; public const int SC_CHARSET_JOHAB = 130; public const int SC_CHARSET_HEBREW = 177; public const int SC_CHARSET_ARABIC = 178; public const int SC_CHARSET_VIETNAMESE = 163; public const int SC_CHARSET_THAI = 222; public const int SC_CHARSET_8859_15 = 1000; public const int SCI_STYLECLEARALL = 2050; public const int SCI_STYLESETFORE = 2051; public const int SCI_STYLESETBACK = 2052; public const int SCI_STYLESETBOLD = 2053; public const int SCI_STYLESETITALIC = 2054; public const int SCI_STYLESETSIZE = 2055; public const int SCI_STYLESETFONT = 2056; public const int SCI_STYLESETEOLFILLED = 2057; public const int SCI_STYLERESETDEFAULT = 2058; public const int SCI_STYLESETUNDERLINE = 2059; public const int SC_CASE_MIXED = 0; public const int SC_CASE_UPPER = 1; public const int SC_CASE_LOWER = 2; public const int SC_CASE_CAMEL = 3; public const int SCI_STYLEGETFORE = 2481; public const int SCI_STYLEGETBACK = 2482; public const int SCI_STYLEGETBOLD = 2483; public const int SCI_STYLEGETITALIC = 2484; public const int SCI_STYLEGETSIZE = 2485; public const int SCI_STYLEGETFONT = 2486; public const int SCI_STYLEGETEOLFILLED = 2487; public const int SCI_STYLEGETUNDERLINE = 2488; public const int SCI_STYLEGETCASE = 2489; public const int SCI_STYLEGETCHARACTERSET = 2490; public const int SCI_STYLEGETVISIBLE = 2491; public const int SCI_STYLEGETCHANGEABLE = 2492; public const int SCI_STYLEGETHOTSPOT = 2493; public const int SCI_STYLESETCASE = 2060; public const int SC_FONT_SIZE_MULTIPLIER = 100; public const int SCI_STYLESETSIZEFRACTIONAL = 2061; public const int SCI_STYLEGETSIZEFRACTIONAL = 2062; public const int SC_WEIGHT_NORMAL = 400; public const int SC_WEIGHT_SEMIBOLD = 600; public const int SC_WEIGHT_BOLD = 700; public const int SCI_STYLESETWEIGHT = 2063; public const int SCI_STYLEGETWEIGHT = 2064; public const int SCI_STYLESETCHARACTERSET = 2066; public const int SCI_STYLESETHOTSPOT = 2409; public const int SCI_STYLESETCHECKMONOSPACED = 2254; public const int SCI_STYLEGETCHECKMONOSPACED = 2255; public const int SC_STRETCH_ULTRA_CONDENSED = 1; public const int SC_STRETCH_EXTRA_CONDENSED = 2; public const int SC_STRETCH_CONDENSED = 3; public const int SC_STRETCH_SEMI_CONDENSED = 4; public const int SC_STRETCH_NORMAL = 5; public const int SC_STRETCH_SEMI_EXPANDED = 6; public const int SC_STRETCH_EXPANDED = 7; public const int SC_STRETCH_EXTRA_EXPANDED = 8; public const int SC_STRETCH_ULTRA_EXPANDED = 9; public const int SCI_STYLESETSTRETCH = 2258; public const int SCI_STYLEGETSTRETCH = 2259; public const int SCI_STYLESETINVISIBLEREPRESENTATION = 2256; public const int SCI_STYLEGETINVISIBLEREPRESENTATION = 2257; public const int SC_ELEMENT_LIST = 0; public const int SC_ELEMENT_LIST_BACK = 1; public const int SC_ELEMENT_LIST_SELECTED = 2; public const int SC_ELEMENT_LIST_SELECTED_BACK = 3; public const int SC_ELEMENT_SELECTION_TEXT = 10; public const int SC_ELEMENT_SELECTION_BACK = 11; public const int SC_ELEMENT_SELECTION_ADDITIONAL_TEXT = 12; public const int SC_ELEMENT_SELECTION_ADDITIONAL_BACK = 13; public const int SC_ELEMENT_SELECTION_SECONDARY_TEXT = 14; public const int SC_ELEMENT_SELECTION_SECONDARY_BACK = 15; public const int SC_ELEMENT_SELECTION_INACTIVE_TEXT = 16; public const int SC_ELEMENT_SELECTION_INACTIVE_BACK = 17; public const int SC_ELEMENT_SELECTION_INACTIVE_ADDITIONAL_TEXT = 18; public const int SC_ELEMENT_SELECTION_INACTIVE_ADDITIONAL_BACK = 19; public const int SC_ELEMENT_CARET = 40; public const int SC_ELEMENT_CARET_ADDITIONAL = 41; public const int SC_ELEMENT_CARET_LINE_BACK = 50; public const int SC_ELEMENT_WHITE_SPACE = 60; public const int SC_ELEMENT_WHITE_SPACE_BACK = 61; public const int SC_ELEMENT_HOT_SPOT_ACTIVE = 70; public const int SC_ELEMENT_HOT_SPOT_ACTIVE_BACK = 71; public const int SC_ELEMENT_FOLD_LINE = 80; public const int SC_ELEMENT_HIDDEN_LINE = 81; public const int SCI_SETELEMENTCOLOUR = 2753; public const int SCI_GETELEMENTCOLOUR = 2754; public const int SCI_RESETELEMENTCOLOUR = 2755; public const int SCI_GETELEMENTISSET = 2756; public const int SCI_GETELEMENTALLOWSTRANSLUCENT = 2757; public const int SCI_GETELEMENTBASECOLOUR = 2758; public const int SCI_SETSELFORE = 2067; public const int SCI_SETSELBACK = 2068; public const int SCI_GETSELALPHA = 2477; public const int SCI_SETSELALPHA = 2478; public const int SCI_GETSELEOLFILLED = 2479; public const int SCI_SETSELEOLFILLED = 2480; public const int SC_LAYER_BASE = 0; public const int SC_LAYER_UNDER_TEXT = 1; public const int SC_LAYER_OVER_TEXT = 2; public const int SCI_GETSELECTIONLAYER = 2762; public const int SCI_SETSELECTIONLAYER = 2763; public const int SCI_GETCARETLINELAYER = 2764; public const int SCI_SETCARETLINELAYER = 2765; public const int SCI_GETCARETLINEHIGHLIGHTSUBLINE = 2773; public const int SCI_SETCARETLINEHIGHLIGHTSUBLINE = 2774; public const int SCI_SETCARETFORE = 2069; public const int SCI_ASSIGNCMDKEY = 2070; public const int SCI_CLEARCMDKEY = 2071; public const int SCI_CLEARALLCMDKEYS = 2072; public const int SCI_SETSTYLINGEX = 2073; public const int SCI_STYLESETVISIBLE = 2074; public const int SCI_GETCARETPERIOD = 2075; public const int SCI_SETCARETPERIOD = 2076; public const int SCI_SETWORDCHARS = 2077; public const int SCI_GETWORDCHARS = 2646; public const int SCI_SETCHARACTERCATEGORYOPTIMIZATION = 2720; public const int SCI_GETCHARACTERCATEGORYOPTIMIZATION = 2721; public const int SCI_BEGINUNDOACTION = 2078; public const int SCI_ENDUNDOACTION = 2079; public const int SCI_GETUNDOSEQUENCE = 2799; public const int SCI_GETUNDOACTIONS = 2790; public const int SCI_SETUNDOSAVEPOINT = 2791; public const int SCI_GETUNDOSAVEPOINT = 2792; public const int SCI_SETUNDODETACH = 2793; public const int SCI_GETUNDODETACH = 2794; public const int SCI_SETUNDOTENTATIVE = 2795; public const int SCI_GETUNDOTENTATIVE = 2796; public const int SCI_SETUNDOCURRENT = 2797; public const int SCI_GETUNDOCURRENT = 2798; public const int SCI_PUSHUNDOACTIONTYPE = 2800; public const int SCI_CHANGELASTUNDOACTIONTEXT = 2801; public const int SCI_GETUNDOACTIONTYPE = 2802; public const int SCI_GETUNDOACTIONPOSITION = 2803; public const int SCI_GETUNDOACTIONTEXT = 2804; public const int INDIC_PLAIN = 0; public const int INDIC_SQUIGGLE = 1; public const int INDIC_TT = 2; public const int INDIC_DIAGONAL = 3; public const int INDIC_STRIKE = 4; public const int INDIC_HIDDEN = 5; public const int INDIC_BOX = 6; public const int INDIC_ROUNDBOX = 7; public const int INDIC_STRAIGHTBOX = 8; public const int INDIC_DASH = 9; public const int INDIC_DOTS = 10; public const int INDIC_SQUIGGLELOW = 11; public const int INDIC_DOTBOX = 12; public const int INDIC_SQUIGGLEPIXMAP = 13; public const int INDIC_COMPOSITIONTHICK = 14; public const int INDIC_COMPOSITIONTHIN = 15; public const int INDIC_FULLBOX = 16; public const int INDIC_TEXTFORE = 17; public const int INDIC_POINT = 18; public const int INDIC_POINTCHARACTER = 19; public const int INDIC_GRADIENT = 20; public const int INDIC_GRADIENTCENTRE = 21; public const int INDIC_POINT_TOP = 22; public const int INDICATOR_CONTAINER = 8; public const int INDICATOR_IME = 32; public const int INDICATOR_IME_MAX = 35; public const int INDICATOR_HISTORY_REVERTED_TO_ORIGIN_INSERTION = 36; public const int INDICATOR_HISTORY_REVERTED_TO_ORIGIN_DELETION = 37; public const int INDICATOR_HISTORY_SAVED_INSERTION = 38; public const int INDICATOR_HISTORY_SAVED_DELETION = 39; public const int INDICATOR_HISTORY_MODIFIED_INSERTION = 40; public const int INDICATOR_HISTORY_MODIFIED_DELETION = 41; public const int INDICATOR_HISTORY_REVERTED_TO_MODIFIED_INSERTION = 42; public const int INDICATOR_HISTORY_REVERTED_TO_MODIFIED_DELETION = 43; public const int INDICATOR_MAX = 43; //deprecated //public const int INDIC_CONTAINER = 8; //public const int INDIC_IME = 32; //public const int INDIC_IME_MAX = 35; //public const int INDIC_MAX = 35; //public const int INDIC0_MASK = 0x20; //public const int INDIC1_MASK = 0x40; //public const int INDIC2_MASK = 0x80; //public const int INDICS_MASK = 0xE0; public const int SCI_INDICSETSTYLE = 2080; public const int SCI_INDICGETSTYLE = 2081; public const int SCI_INDICSETFORE = 2082; public const int SCI_INDICGETFORE = 2083; public const int SCI_INDICSETUNDER = 2510; public const int SCI_INDICGETUNDER = 2511; public const int SCI_INDICSETHOVERSTYLE = 2680; public const int SCI_INDICGETHOVERSTYLE = 2681; public const int SCI_INDICSETHOVERFORE = 2682; public const int SCI_INDICGETHOVERFORE = 2683; public const int SC_INDICVALUEBIT = 0x1000000; public const int SC_INDICVALUEMASK = 0xFFFFFF; public const int SC_INDICFLAG_NONE = 0; public const int SC_INDICFLAG_VALUEFORE = 1; public const int SCI_INDICSETFLAGS = 2684; public const int SCI_INDICGETFLAGS = 2685; public const int SCI_INDICSETSTROKEWIDTH = 2751; public const int SCI_INDICGETSTROKEWIDTH = 2752; public const int SCI_SETWHITESPACEFORE = 2084; public const int SCI_SETWHITESPACEBACK = 2085; public const int SCI_SETWHITESPACESIZE = 2086; public const int SCI_GETWHITESPACESIZE = 2087; //deprecated //public const int SCI_SETSTYLEBITS = 2090; //public const int SCI_GETSTYLEBITS = 2091; public const int SCI_SETLINESTATE = 2092; public const int SCI_GETLINESTATE = 2093; public const int SCI_GETMAXLINESTATE = 2094; public const int SCI_GETCARETLINEVISIBLE = 2095; public const int SCI_SETCARETLINEVISIBLE = 2096; public const int SCI_GETCARETLINEBACK = 2097; public const int SCI_SETCARETLINEBACK = 2098; public const int SCI_GETCARETLINEFRAME = 2704; public const int SCI_SETCARETLINEFRAME = 2705; public const int SCI_STYLESETCHANGEABLE = 2099; public const int SCI_AUTOCSHOW = 2100; public const int SCI_AUTOCCANCEL = 2101; public const int SCI_AUTOCACTIVE = 2102; public const int SCI_AUTOCPOSSTART = 2103; public const int SCI_AUTOCCOMPLETE = 2104; public const int SCI_AUTOCSTOPS = 2105; public const int SCI_AUTOCSETSEPARATOR = 2106; public const int SCI_AUTOCGETSEPARATOR = 2107; public const int SCI_AUTOCSELECT = 2108; public const int SCI_AUTOCSETCANCELATSTART = 2110; public const int SCI_AUTOCGETCANCELATSTART = 2111; public const int SCI_AUTOCSETFILLUPS = 2112; public const int SCI_AUTOCSETCHOOSESINGLE = 2113; public const int SCI_AUTOCGETCHOOSESINGLE = 2114; public const int SCI_AUTOCSETIGNORECASE = 2115; public const int SCI_AUTOCGETIGNORECASE = 2116; public const int SCI_USERLISTSHOW = 2117; public const int SCI_AUTOCSETAUTOHIDE = 2118; public const int SCI_AUTOCGETAUTOHIDE = 2119; public const int SC_AUTOCOMPLETE_NORMAL = 0; public const int SC_AUTOCOMPLETE_FIXED_SIZE = 1; public const int SC_AUTOCOMPLETE_SELECT_FIRST_ITEM = 2; public const int SCI_AUTOCSETOPTIONS = 2638; public const int SCI_AUTOCGETOPTIONS = 2639; public const int SCI_AUTOCSETDROPRESTOFWORD = 2270; public const int SCI_AUTOCGETDROPRESTOFWORD = 2271; public const int SCI_REGISTERIMAGE = 2405; public const int SCI_CLEARREGISTEREDIMAGES = 2408; public const int SCI_AUTOCGETTYPESEPARATOR = 2285; public const int SCI_AUTOCSETTYPESEPARATOR = 2286; public const int SCI_AUTOCSETMAXWIDTH = 2208; public const int SCI_AUTOCGETMAXWIDTH = 2209; public const int SCI_AUTOCSETMAXHEIGHT = 2210; public const int SCI_AUTOCGETMAXHEIGHT = 2211; public const int SCI_AUTOCSETSTYLE = 2109; public const int SCI_AUTOCGETSTYLE = 2120; public const int SCI_AUTOCSETIMAGESCALE = 2815; public const int SCI_AUTOCGETIMAGESCALE = 2816; public const int SCI_SETINDENT = 2122; public const int SCI_GETINDENT = 2123; public const int SCI_SETUSETABS = 2124; public const int SCI_GETUSETABS = 2125; public const int SCI_SETLINEINDENTATION = 2126; public const int SCI_GETLINEINDENTATION = 2127; public const int SCI_GETLINEINDENTPOSITION = 2128; public const int SCI_GETCOLUMN = 2129; public const int SCI_COUNTCHARACTERS = 2633; public const int SCI_COUNTCODEUNITS = 2715; public const int SCI_SETHSCROLLBAR = 2130; public const int SCI_GETHSCROLLBAR = 2131; public const int SC_IV_NONE = 0; public const int SC_IV_REAL = 1; public const int SC_IV_LOOKFORWARD = 2; public const int SC_IV_LOOKBOTH = 3; public const int SCI_SETINDENTATIONGUIDES = 2132; public const int SCI_GETINDENTATIONGUIDES = 2133; public const int SCI_SETHIGHLIGHTGUIDE = 2134; public const int SCI_GETHIGHLIGHTGUIDE = 2135; public const int SCI_GETLINEENDPOSITION = 2136; public const int SCI_GETCODEPAGE = 2137; public const int SCI_GETCARETFORE = 2138; public const int SCI_GETREADONLY = 2140; public const int SCI_SETCURRENTPOS = 2141; public const int SCI_SETSELECTIONSTART = 2142; public const int SCI_GETSELECTIONSTART = 2143; public const int SCI_SETSELECTIONEND = 2144; public const int SCI_GETSELECTIONEND = 2145; public const int SCI_SETEMPTYSELECTION = 2556; public const int SCI_SETPRINTMAGNIFICATION = 2146; public const int SCI_GETPRINTMAGNIFICATION = 2147; public const int SC_PRINT_NORMAL = 0; public const int SC_PRINT_INVERTLIGHT = 1; public const int SC_PRINT_BLACKONWHITE = 2; public const int SC_PRINT_COLOURONWHITE = 3; public const int SC_PRINT_COLOURONWHITEDEFAULTBG = 4; public const int SC_PRINT_SCREENCOLOURS = 5; public const int SCI_SETPRINTCOLOURMODE = 2148; public const int SCI_GETPRINTCOLOURMODE = 2149; public const int SCFIND_WHOLEWORD = 0x2; public const int SCFIND_MATCHCASE = 0x4; public const int SCFIND_WORDSTART = 0x00100000; public const int SCFIND_REGEXP = 0x00200000; public const int SCFIND_POSIX = 0x00400000; public const int SCFIND_CXX11REGEX = 0x00800000; public const int SCI_FINDTEXT = 2150; public const int SCI_FINDTEXTFULL = 2196; public const int SCI_FORMATRANGE = 2151; public const int SCI_FORMATRANGEFULL = 2777; public const int SC_CHANGE_HISTORY_DISABLED = 0; public const int SC_CHANGE_HISTORY_ENABLED = 1; public const int SC_CHANGE_HISTORY_MARKERS = 2; public const int SC_CHANGE_HISTORY_INDICATORS = 4; public const int SCI_SETCHANGEHISTORY = 2780; public const int SCI_GETCHANGEHISTORY = 2781; public const int SC_UNDO_SELECTION_HISTORY_DISABLED = 0; public const int SC_UNDO_SELECTION_HISTORY_ENABLED = 1; public const int SC_UNDO_SELECTION_HISTORY_SCROLL = 2; public const int SCI_SETUNDOSELECTIONHISTORY = 2782; public const int SCI_GETUNDOSELECTIONHISTORY = 2783; public const int SCI_SETSELECTIONSERIALIZED = 2784; public const int SCI_GETSELECTIONSERIALIZED = 2785; public const int SCI_GETFIRSTVISIBLELINE = 2152; public const int SCI_GETLINE = 2153; public const int SCI_GETLINECOUNT = 2154; public const int SCI_ALLOCATELINES = 2089; public const int SCI_SETMARGINLEFT = 2155; public const int SCI_GETMARGINLEFT = 2156; public const int SCI_SETMARGINRIGHT = 2157; public const int SCI_GETMARGINRIGHT = 2158; public const int SCI_GETMODIFY = 2159; public const int SCI_SETSEL = 2160; public const int SCI_GETSELTEXT = 2161; public const int SCI_GETTEXTRANGE = 2162; public const int SCI_GETTEXTRANGEFULL = 2039; public const int SCI_HIDESELECTION = 2163; public const int SCI_GETSELECTIONHIDDEN = 2088; public const int SCI_POINTXFROMPOSITION = 2164; public const int SCI_POINTYFROMPOSITION = 2165; public const int SCI_LINEFROMPOSITION = 2166; public const int SCI_POSITIONFROMLINE = 2167; public const int SCI_LINESCROLL = 2168; public const int SCI_SCROLLVERTICAL = 2817; public const int SCI_SCROLLCARET = 2169; public const int SCI_SCROLLRANGE = 2569; public const int SCI_REPLACESEL = 2170; public const int SCI_SETREADONLY = 2171; public const int SCI_NULL = 2172; public const int SCI_CANPASTE = 2173; public const int SCI_CANUNDO = 2174; public const int SCI_EMPTYUNDOBUFFER = 2175; public const int SCI_UNDO = 2176; public const int SCI_CUT = 2177; public const int SCI_COPY = 2178; public const int SCI_PASTE = 2179; public const int SCI_CLEAR = 2180; public const int SCI_SETTEXT = 2181; public const int SCI_GETTEXT = 2182; public const int SCI_GETTEXTLENGTH = 2183; public const int SCI_GETDIRECTFUNCTION = 2184; public const int SCI_GETDIRECTSTATUSFUNCTION = 2772; public const int SCI_GETDIRECTPOINTER = 2185; public const int SCI_SETOVERTYPE = 2186; public const int SCI_GETOVERTYPE = 2187; public const int SCI_SETCARETWIDTH = 2188; public const int SCI_GETCARETWIDTH = 2189; public const int SCI_SETTARGETSTART = 2190; public const int SCI_GETTARGETSTART = 2191; public const int SCI_SETTARGETEND = 2192; public const int SCI_GETTARGETEND = 2193; public const int SCI_SETTARGETRANGE = 2686; public const int SCI_GETTARGETTEXT = 2687; public const int SCI_TARGETFROMSELECTION = 2287; public const int SCI_TARGETWHOLEDOCUMENT = 2690; public const int SCI_REPLACETARGET = 2194; public const int SCI_REPLACETARGETRE = 2195; public const int SCI_REPLACETARGETMINIMAL = 2779; public const int SCI_SEARCHINTARGET = 2197; public const int SCI_SETSEARCHFLAGS = 2198; public const int SCI_GETSEARCHFLAGS = 2199; public const int SCI_CALLTIPSHOW = 2200; public const int SCI_CALLTIPCANCEL = 2201; public const int SCI_CALLTIPACTIVE = 2202; public const int SCI_CALLTIPPOSSTART = 2203; public const int SCI_CALLTIPSETPOSSTART = 2214; public const int SCI_CALLTIPSETHLT = 2204; public const int SCI_CALLTIPSETBACK = 2205; public const int SCI_CALLTIPSETFORE = 2206; public const int SCI_CALLTIPSETFOREHLT = 2207; public const int SCI_CALLTIPUSESTYLE = 2212; public const int SCI_CALLTIPSETPOSITION = 2213; public const int SCI_VISIBLEFROMDOCLINE = 2220; public const int SCI_DOCLINEFROMVISIBLE = 2221; public const int SCI_WRAPCOUNT = 2235; public const int SC_FOLDLEVELNONE = 0x0; public const int SC_FOLDLEVELBASE = 0x400; public const int SC_FOLDLEVELWHITEFLAG = 0x1000; public const int SC_FOLDLEVELHEADERFLAG = 0x2000; public const int SC_FOLDLEVELNUMBERMASK = 0x0FFF; public const int SCI_SETFOLDLEVEL = 2222; public const int SCI_GETFOLDLEVEL = 2223; public const int SCI_GETLASTCHILD = 2224; public const int SCI_GETFOLDPARENT = 2225; public const int SCI_SHOWLINES = 2226; public const int SCI_HIDELINES = 2227; public const int SCI_GETLINEVISIBLE = 2228; public const int SCI_GETALLLINESVISIBLE = 2236; public const int SCI_SETFOLDEXPANDED = 2229; public const int SCI_GETFOLDEXPANDED = 2230; public const int SCI_TOGGLEFOLD = 2231; public const int SCI_TOGGLEFOLDSHOWTEXT = 2700; public const int SC_FOLDDISPLAYTEXT_HIDDEN = 0; public const int SC_FOLDDISPLAYTEXT_STANDARD = 1; public const int SC_FOLDDISPLAYTEXT_BOXED = 2; public const int SCI_FOLDDISPLAYTEXTSETSTYLE = 2701; public const int SCI_FOLDDISPLAYTEXTGETSTYLE = 2707; public const int SCI_SETDEFAULTFOLDDISPLAYTEXT = 2722; public const int SCI_GETDEFAULTFOLDDISPLAYTEXT = 2723; public const int SC_FOLDACTION_CONTRACT = 0; public const int SC_FOLDACTION_EXPAND = 1; public const int SC_FOLDACTION_TOGGLE = 2; public const int SC_FOLDACTION_CONTRACT_EVERY_LEVEL = 4; public const int SCI_FOLDLINE = 2237; public const int SCI_FOLDCHILDREN = 2238; public const int SCI_EXPANDCHILDREN = 2239; public const int SCI_FOLDALL = 2662; public const int SCI_ENSUREVISIBLE = 2232; public const int SC_AUTOMATICFOLD_NONE = 0x0000; public const int SC_AUTOMATICFOLD_SHOW = 0x0001; public const int SC_AUTOMATICFOLD_CLICK = 0x0002; public const int SC_AUTOMATICFOLD_CHANGE = 0x0004; public const int SCI_SETAUTOMATICFOLD = 2663; public const int SCI_GETAUTOMATICFOLD = 2664; public const int SC_FOLDFLAG_NONE = 0x0000; public const int SC_FOLDFLAG_LINEBEFORE_EXPANDED = 0x0002; public const int SC_FOLDFLAG_LINEBEFORE_CONTRACTED = 0x0004; public const int SC_FOLDFLAG_LINEAFTER_EXPANDED = 0x0008; public const int SC_FOLDFLAG_LINEAFTER_CONTRACTED = 0x0010; public const int SC_FOLDFLAG_LEVELNUMBERS = 0x0040; public const int SC_FOLDFLAG_LINESTATE = 0x0080; public const int SCI_SETFOLDFLAGS = 2233; public const int SCI_ENSUREVISIBLEENFORCEPOLICY = 2234; public const int SCI_SETTABINDENTS = 2260; public const int SCI_GETTABINDENTS = 2261; public const int SCI_SETBACKSPACEUNINDENTS = 2262; public const int SCI_GETBACKSPACEUNINDENTS = 2263; public const int SC_TIME_FOREVER = 10000000; public const int SCI_SETMOUSEDWELLTIME = 2264; public const int SCI_GETMOUSEDWELLTIME = 2265; public const int SCI_WORDSTARTPOSITION = 2266; public const int SCI_WORDENDPOSITION = 2267; public const int SCI_ISRANGEWORD = 2691; public const int SC_IDLESTYLING_NONE = 0; public const int SC_IDLESTYLING_TOVISIBLE = 1; public const int SC_IDLESTYLING_AFTERVISIBLE = 2; public const int SC_IDLESTYLING_ALL = 3; public const int SCI_SETIDLESTYLING = 2692; public const int SCI_GETIDLESTYLING = 2693; public const int SC_WRAP_NONE = 0; public const int SC_WRAP_WORD = 1; public const int SC_WRAP_CHAR = 2; public const int SC_WRAP_WHITESPACE = 3; public const int SCI_SETWRAPMODE = 2268; public const int SCI_GETWRAPMODE = 2269; public const int SC_WRAPVISUALFLAG_NONE = 0x0000; public const int SC_WRAPVISUALFLAG_END = 0x0001; public const int SC_WRAPVISUALFLAG_START = 0x0002; public const int SC_WRAPVISUALFLAG_MARGIN = 0x0004; public const int SCI_SETWRAPVISUALFLAGS = 2460; public const int SCI_GETWRAPVISUALFLAGS = 2461; public const int SC_WRAPVISUALFLAGLOC_DEFAULT = 0x0000; public const int SC_WRAPVISUALFLAGLOC_END_BY_TEXT = 0x0001; public const int SC_WRAPVISUALFLAGLOC_START_BY_TEXT = 0x0002; public const int SCI_SETWRAPVISUALFLAGSLOCATION = 2462; public const int SCI_GETWRAPVISUALFLAGSLOCATION = 2463; public const int SCI_SETWRAPSTARTINDENT = 2464; public const int SCI_GETWRAPSTARTINDENT = 2465; public const int SC_WRAPINDENT_FIXED = 0; public const int SC_WRAPINDENT_SAME = 1; public const int SC_WRAPINDENT_INDENT = 2; public const int SC_WRAPINDENT_DEEPINDENT = 3; public const int SCI_SETWRAPINDENTMODE = 2472; public const int SCI_GETWRAPINDENTMODE = 2473; public const int SC_CACHE_NONE = 0; public const int SC_CACHE_CARET = 1; public const int SC_CACHE_PAGE = 2; public const int SC_CACHE_DOCUMENT = 3; public const int SCI_SETLAYOUTCACHE = 2272; public const int SCI_GETLAYOUTCACHE = 2273; public const int SCI_SETSCROLLWIDTH = 2274; public const int SCI_GETSCROLLWIDTH = 2275; public const int SCI_SETSCROLLWIDTHTRACKING = 2516; public const int SCI_GETSCROLLWIDTHTRACKING = 2517; public const int SCI_TEXTWIDTH = 2276; public const int SCI_SETENDATLASTLINE = 2277; public const int SCI_GETENDATLASTLINE = 2278; public const int SCI_TEXTHEIGHT = 2279; public const int SCI_SETVSCROLLBAR = 2280; public const int SCI_GETVSCROLLBAR = 2281; public const int SCI_APPENDTEXT = 2282; public const int SC_PHASES_ONE = 0; public const int SC_PHASES_TWO = 1; public const int SC_PHASES_MULTIPLE = 2; public const int SCI_GETPHASESDRAW = 2673; public const int SCI_SETPHASESDRAW = 2674; public const int SC_EFF_QUALITY_MASK = 0xF; public const int SC_EFF_QUALITY_DEFAULT = 0; public const int SC_EFF_QUALITY_NON_ANTIALIASED = 1; public const int SC_EFF_QUALITY_ANTIALIASED = 2; public const int SC_EFF_QUALITY_LCD_OPTIMIZED = 3; public const int SCI_SETFONTQUALITY = 2611; public const int SCI_GETFONTQUALITY = 2612; public const int SCI_SETFIRSTVISIBLELINE = 2613; public const int SC_MULTIPASTE_ONCE = 0; public const int SC_MULTIPASTE_EACH = 1; public const int SCI_SETMULTIPASTE = 2614; public const int SCI_GETMULTIPASTE = 2615; public const int SCI_GETTAG = 2616; public const int SCI_LINESJOIN = 2288; public const int SCI_LINESSPLIT = 2289; public const int SCI_SETFOLDMARGINCOLOUR = 2290; public const int SCI_SETFOLDMARGINHICOLOUR = 2291; public const int SC_ACCESSIBILITY_DISABLED = 0; public const int SC_ACCESSIBILITY_ENABLED = 1; public const int SCI_SETACCESSIBILITY = 2702; public const int SCI_GETACCESSIBILITY = 2703; public const int SCI_LINEDOWN = 2300; public const int SCI_LINEDOWNEXTEND = 2301; public const int SCI_LINEUP = 2302; public const int SCI_LINEUPEXTEND = 2303; public const int SCI_CHARLEFT = 2304; public const int SCI_CHARLEFTEXTEND = 2305; public const int SCI_CHARRIGHT = 2306; public const int SCI_CHARRIGHTEXTEND = 2307; public const int SCI_WORDLEFT = 2308; public const int SCI_WORDLEFTEXTEND = 2309; public const int SCI_WORDRIGHT = 2310; public const int SCI_WORDRIGHTEXTEND = 2311; public const int SCI_HOME = 2312; public const int SCI_HOMEEXTEND = 2313; public const int SCI_LINEEND = 2314; public const int SCI_LINEENDEXTEND = 2315; public const int SCI_DOCUMENTSTART = 2316; public const int SCI_DOCUMENTSTARTEXTEND = 2317; public const int SCI_DOCUMENTEND = 2318; public const int SCI_DOCUMENTENDEXTEND = 2319; public const int SCI_PAGEUP = 2320; public const int SCI_PAGEUPEXTEND = 2321; public const int SCI_PAGEDOWN = 2322; public const int SCI_PAGEDOWNEXTEND = 2323; public const int SCI_EDITTOGGLEOVERTYPE = 2324; public const int SCI_CANCEL = 2325; public const int SCI_DELETEBACK = 2326; public const int SCI_TAB = 2327; public const int SCI_LINEINDENT = 2813; public const int SCI_BACKTAB = 2328; public const int SCI_LINEDEDENT = 2814; public const int SCI_NEWLINE = 2329; public const int SCI_FORMFEED = 2330; public const int SCI_VCHOME = 2331; public const int SCI_VCHOMEEXTEND = 2332; public const int SCI_ZOOMIN = 2333; public const int SCI_ZOOMOUT = 2334; public const int SCI_DELWORDLEFT = 2335; public const int SCI_DELWORDRIGHT = 2336; public const int SCI_DELWORDRIGHTEND = 2518; public const int SCI_LINECUT = 2337; public const int SCI_LINEDELETE = 2338; public const int SCI_LINETRANSPOSE = 2339; public const int SCI_LINEREVERSE = 2354; public const int SCI_LINEDUPLICATE = 2404; public const int SCI_LOWERCASE = 2340; public const int SCI_UPPERCASE = 2341; public const int SCI_LINESCROLLDOWN = 2342; public const int SCI_LINESCROLLUP = 2343; public const int SCI_DELETEBACKNOTLINE = 2344; public const int SCI_HOMEDISPLAY = 2345; public const int SCI_HOMEDISPLAYEXTEND = 2346; public const int SCI_LINEENDDISPLAY = 2347; public const int SCI_LINEENDDISPLAYEXTEND = 2348; public const int SCI_HOMEWRAP = 2349; public const int SCI_HOMEWRAPEXTEND = 2450; public const int SCI_LINEENDWRAP = 2451; public const int SCI_LINEENDWRAPEXTEND = 2452; public const int SCI_VCHOMEWRAP = 2453; public const int SCI_VCHOMEWRAPEXTEND = 2454; public const int SCI_LINECOPY = 2455; public const int SCI_MOVECARETINSIDEVIEW = 2401; public const int SCI_LINELENGTH = 2350; public const int SCI_BRACEHIGHLIGHT = 2351; public const int SCI_BRACEHIGHLIGHTINDICATOR = 2498; public const int SCI_BRACEBADLIGHT = 2352; public const int SCI_BRACEBADLIGHTINDICATOR = 2499; public const int SCI_BRACEMATCH = 2353; public const int SCI_BRACEMATCHNEXT = 2369; public const int SCI_GETVIEWEOL = 2355; public const int SCI_SETVIEWEOL = 2356; public const int SCI_GETDOCPOINTER = 2357; public const int SCI_SETDOCPOINTER = 2358; public const int SCI_SETMODEVENTMASK = 2359; public const int EDGE_NONE = 0; public const int EDGE_LINE = 1; public const int EDGE_BACKGROUND = 2; public const int EDGE_MULTILINE = 3; public const int SCI_GETEDGECOLUMN = 2360; public const int SCI_SETEDGECOLUMN = 2361; public const int SCI_GETEDGEMODE = 2362; public const int SCI_SETEDGEMODE = 2363; public const int SCI_GETEDGECOLOUR = 2364; public const int SCI_SETEDGECOLOUR = 2365; public const int SCI_MULTIEDGEADDLINE = 2694; public const int SCI_MULTIEDGECLEARALL = 2695; public const int SCI_GETMULTIEDGECOLUMN = 2749; public const int SCI_SEARCHANCHOR = 2366; public const int SCI_SEARCHNEXT = 2367; public const int SCI_SEARCHPREV = 2368; public const int SCI_LINESONSCREEN = 2370; public const int SC_POPUP_NEVER = 0; public const int SC_POPUP_ALL = 1; public const int SC_POPUP_TEXT = 2; public const int SCI_USEPOPUP = 2371; public const int SCI_SELECTIONISRECTANGLE = 2372; public const int SCI_SETZOOM = 2373; public const int SCI_GETZOOM = 2374; public const int SC_DOCUMENTOPTION_DEFAULT = 0; public const int SC_DOCUMENTOPTION_STYLES_NONE = 0x1; public const int SC_DOCUMENTOPTION_TEXT_LARGE = 0x100; public const int SCI_CREATEDOCUMENT = 2375; public const int SCI_ADDREFDOCUMENT = 2376; public const int SCI_RELEASEDOCUMENT = 2377; public const int SCI_GETDOCUMENTOPTIONS = 2379; public const int SCI_GETMODEVENTMASK = 2378; public const int SCI_SETCOMMANDEVENTS = 2717; public const int SCI_GETCOMMANDEVENTS = 2718; public const int SCI_SETFOCUS = 2380; public const int SCI_GETFOCUS = 2381; public const int SC_STATUS_OK = 0; public const int SC_STATUS_FAILURE = 1; public const int SC_STATUS_BADALLOC = 2; public const int SC_STATUS_WARN_START = 1000; public const int SC_STATUS_WARN_REGEX = 1001; public const int SCI_SETSTATUS = 2382; public const int SCI_GETSTATUS = 2383; public const int SCI_SETMOUSEDOWNCAPTURES = 2384; public const int SCI_GETMOUSEDOWNCAPTURES = 2385; public const int SCI_SETMOUSEWHEELCAPTURES = 2696; public const int SCI_GETMOUSEWHEELCAPTURES = 2697; public const int SC_CURSORNORMAL = -1; public const int SC_CURSORARROW = 2; public const int SC_CURSORWAIT = 4; public const int SC_CURSORREVERSEARROW = 7; public const int SCI_SETCURSOR = 2386; public const int SCI_GETCURSOR = 2387; public const int SCI_SETCONTROLCHARSYMBOL = 2388; public const int SCI_GETCONTROLCHARSYMBOL = 2389; public const int SCI_WORDPARTLEFT = 2390; public const int SCI_WORDPARTLEFTEXTEND = 2391; public const int SCI_WORDPARTRIGHT = 2392; public const int SCI_WORDPARTRIGHTEXTEND = 2393; public const int VISIBLE_SLOP = 0x01; public const int VISIBLE_STRICT = 0x04; public const int SCI_SETVISIBLEPOLICY = 2394; public const int SCI_DELLINELEFT = 2395; public const int SCI_DELLINERIGHT = 2396; public const int SCI_SETXOFFSET = 2397; public const int SCI_GETXOFFSET = 2398; public const int SCI_CHOOSECARETX = 2399; public const int SCI_GRABFOCUS = 2400; public const int CARET_SLOP = 0x01; public const int CARET_STRICT = 0x04; public const int CARET_JUMPS = 0x10; public const int CARET_EVEN = 0x08; public const int SCI_SETXCARETPOLICY = 2402; public const int SCI_SETYCARETPOLICY = 2403; public const int SCI_SETPRINTWRAPMODE = 2406; public const int SCI_GETPRINTWRAPMODE = 2407; public const int SCI_SETHOTSPOTACTIVEFORE = 2410; public const int SCI_GETHOTSPOTACTIVEFORE = 2494; public const int SCI_SETHOTSPOTACTIVEBACK = 2411; public const int SCI_GETHOTSPOTACTIVEBACK = 2495; public const int SCI_SETHOTSPOTACTIVEUNDERLINE = 2412; public const int SCI_GETHOTSPOTACTIVEUNDERLINE = 2496; public const int SCI_SETHOTSPOTSINGLELINE = 2421; public const int SCI_GETHOTSPOTSINGLELINE = 2497; public const int SCI_PARADOWN = 2413; public const int SCI_PARADOWNEXTEND = 2414; public const int SCI_PARAUP = 2415; public const int SCI_PARAUPEXTEND = 2416; public const int SCI_POSITIONBEFORE = 2417; public const int SCI_POSITIONAFTER = 2418; public const int SCI_POSITIONRELATIVE = 2670; public const int SCI_POSITIONRELATIVECODEUNITS = 2716; public const int SCI_COPYRANGE = 2419; public const int SCI_COPYTEXT = 2420; public const int SC_SEL_STREAM = 0; public const int SC_SEL_RECTANGLE = 1; public const int SC_SEL_LINES = 2; public const int SC_SEL_THIN = 3; public const int SCI_SETSELECTIONMODE = 2422; public const int SCI_CHANGESELECTIONMODE = 2659; public const int SCI_GETSELECTIONMODE = 2423; public const int SCI_SETMOVEEXTENDSSELECTION = 2719; public const int SCI_GETMOVEEXTENDSSELECTION = 2706; public const int SCI_GETLINESELSTARTPOSITION = 2424; public const int SCI_GETLINESELENDPOSITION = 2425; public const int SCI_LINEDOWNRECTEXTEND = 2426; public const int SCI_LINEUPRECTEXTEND = 2427; public const int SCI_CHARLEFTRECTEXTEND = 2428; public const int SCI_CHARRIGHTRECTEXTEND = 2429; public const int SCI_HOMERECTEXTEND = 2430; public const int SCI_VCHOMERECTEXTEND = 2431; public const int SCI_LINEENDRECTEXTEND = 2432; public const int SCI_PAGEUPRECTEXTEND = 2433; public const int SCI_PAGEDOWNRECTEXTEND = 2434; public const int SCI_STUTTEREDPAGEUP = 2435; public const int SCI_STUTTEREDPAGEUPEXTEND = 2436; public const int SCI_STUTTEREDPAGEDOWN = 2437; public const int SCI_STUTTEREDPAGEDOWNEXTEND = 2438; public const int SCI_WORDLEFTEND = 2439; public const int SCI_WORDLEFTENDEXTEND = 2440; public const int SCI_WORDRIGHTEND = 2441; public const int SCI_WORDRIGHTENDEXTEND = 2442; public const int SCI_SETWHITESPACECHARS = 2443; public const int SCI_GETWHITESPACECHARS = 2647; public const int SCI_SETPUNCTUATIONCHARS = 2648; public const int SCI_GETPUNCTUATIONCHARS = 2649; public const int SCI_SETCHARSDEFAULT = 2444; public const int SCI_AUTOCGETCURRENT = 2445; public const int SCI_AUTOCGETCURRENTTEXT = 2610; public const int SC_CASEINSENSITIVEBEHAVIOUR_RESPECTCASE = 0; public const int SC_CASEINSENSITIVEBEHAVIOUR_IGNORECASE = 1; public const int SCI_AUTOCSETCASEINSENSITIVEBEHAVIOUR = 2634; public const int SCI_AUTOCGETCASEINSENSITIVEBEHAVIOUR = 2635; public const int SC_MULTIAUTOC_ONCE = 0; public const int SC_MULTIAUTOC_EACH = 1; public const int SCI_AUTOCSETMULTI = 2636; public const int SCI_AUTOCGETMULTI = 2637; public const int SC_ORDER_PRESORTED = 0; public const int SC_ORDER_PERFORMSORT = 1; public const int SC_ORDER_CUSTOM = 2; public const int SCI_AUTOCSETORDER = 2660; public const int SCI_AUTOCGETORDER = 2661; public const int SCI_ALLOCATE = 2446; public const int SCI_TARGETASUTF8 = 2447; public const int SCI_SETLENGTHFORENCODE = 2448; public const int SCI_ENCODEDFROMUTF8 = 2449; public const int SCI_FINDCOLUMN = 2456; public const int SCI_GETCARETSTICKY = 2457; public const int SCI_SETCARETSTICKY = 2458; public const int SC_CARETSTICKY_OFF = 0; public const int SC_CARETSTICKY_ON = 1; public const int SC_CARETSTICKY_WHITESPACE = 2; public const int SCI_TOGGLECARETSTICKY = 2459; public const int SCI_SETPASTECONVERTENDINGS = 2467; public const int SCI_GETPASTECONVERTENDINGS = 2468; public const int SCI_REPLACERECTANGULAR = 2771; public const int SCI_SELECTIONDUPLICATE = 2469; public const int SC_ALPHA_TRANSPARENT = 0; public const int SC_ALPHA_OPAQUE = 255; public const int SC_ALPHA_NOALPHA = 256; public const int SCI_SETCARETLINEBACKALPHA = 2470; public const int SCI_GETCARETLINEBACKALPHA = 2471; public const int CARETSTYLE_INVISIBLE = 0; public const int CARETSTYLE_LINE = 1; public const int CARETSTYLE_BLOCK = 2; public const int CARETSTYLE_OVERSTRIKE_BAR = 0; public const int CARETSTYLE_OVERSTRIKE_BLOCK = 16; public const int CARETSTYLE_CURSES = 0x20; public const int CARETSTYLE_INS_MASK = 0xF; public const int SCI_SETCARETSTYLE = 2512; public const int SCI_GETCARETSTYLE = 2513; public const int SCI_SETINDICATORCURRENT = 2500; public const int SCI_GETINDICATORCURRENT = 2501; public const int SCI_SETINDICATORVALUE = 2502; public const int SCI_GETINDICATORVALUE = 2503; public const int SCI_INDICATORFILLRANGE = 2504; public const int SCI_INDICATORCLEARRANGE = 2505; public const int SCI_INDICATORALLONFOR = 2506; public const int SCI_INDICATORVALUEAT = 2507; public const int SCI_INDICATORSTART = 2508; public const int SCI_INDICATOREND = 2509; public const int SCI_SETPOSITIONCACHE = 2514; public const int SCI_GETPOSITIONCACHE = 2515; public const int SCI_SETLAYOUTTHREADS = 2775; public const int SCI_GETLAYOUTTHREADS = 2776; public const int SCI_COPYALLOWLINE = 2519; public const int SCI_CUTALLOWLINE = 2810; public const int SCI_SETCOPYSEPARATOR = 2811; public const int SCI_GETCOPYSEPARATOR = 2812; public const int SCI_GETCHARACTERPOINTER = 2520; public const int SCI_GETRANGEPOINTER = 2643; public const int SCI_GETGAPPOSITION = 2644; public const int SCI_INDICSETALPHA = 2523; public const int SCI_INDICGETALPHA = 2524; public const int SCI_INDICSETOUTLINEALPHA = 2558; public const int SCI_INDICGETOUTLINEALPHA = 2559; public const int SCI_SETEXTRAASCENT = 2525; public const int SCI_GETEXTRAASCENT = 2526; public const int SCI_SETEXTRADESCENT = 2527; public const int SCI_GETEXTRADESCENT = 2528; public const int SCI_MARKERSYMBOLDEFINED = 2529; public const int SCI_MARGINSETTEXT = 2530; public const int SCI_MARGINGETTEXT = 2531; public const int SCI_MARGINSETSTYLE = 2532; public const int SCI_MARGINGETSTYLE = 2533; public const int SCI_MARGINSETSTYLES = 2534; public const int SCI_MARGINGETSTYLES = 2535; public const int SCI_MARGINTEXTCLEARALL = 2536; public const int SCI_MARGINSETSTYLEOFFSET = 2537; public const int SCI_MARGINGETSTYLEOFFSET = 2538; public const int SC_MARGINOPTION_NONE = 0; public const int SC_MARGINOPTION_SUBLINESELECT = 1; public const int SCI_SETMARGINOPTIONS = 2539; public const int SCI_GETMARGINOPTIONS = 2557; public const int SCI_ANNOTATIONSETTEXT = 2540; public const int SCI_ANNOTATIONGETTEXT = 2541; public const int SCI_ANNOTATIONSETSTYLE = 2542; public const int SCI_ANNOTATIONGETSTYLE = 2543; public const int SCI_ANNOTATIONSETSTYLES = 2544; public const int SCI_ANNOTATIONGETSTYLES = 2545; public const int SCI_ANNOTATIONGETLINES = 2546; public const int SCI_ANNOTATIONCLEARALL = 2547; public enum AnnotationsVisible { ANNOTATION_HIDDEN = 0, ANNOTATION_STANDARD = 1, ANNOTATION_BOXED = 2, ANNOTATION_INDENTED = 3, } public const int SCI_ANNOTATIONSETVISIBLE = 2548; public const int SCI_ANNOTATIONGETVISIBLE = 2549; public const int SCI_ANNOTATIONSETSTYLEOFFSET = 2550; public const int SCI_ANNOTATIONGETSTYLEOFFSET = 2551; public const int SCI_RELEASEALLEXTENDEDSTYLES = 2552; public const int SCI_ALLOCATEEXTENDEDSTYLES = 2553; public const int UNDO_MAY_COALESCE = 1; public const int SCI_ADDUNDOACTION = 2560; public const int SCI_CHARPOSITIONFROMPOINT = 2561; public const int SCI_CHARPOSITIONFROMPOINTCLOSE = 2562; public const int SCI_SETMOUSESELECTIONRECTANGULARSWITCH = 2668; public const int SCI_GETMOUSESELECTIONRECTANGULARSWITCH = 2669; public const int SCI_SETMULTIPLESELECTION = 2563; public const int SCI_GETMULTIPLESELECTION = 2564; public const int SCI_SETADDITIONALSELECTIONTYPING = 2565; public const int SCI_GETADDITIONALSELECTIONTYPING = 2566; public const int SCI_SETADDITIONALCARETSBLINK = 2567; public const int SCI_GETADDITIONALCARETSBLINK = 2568; public const int SCI_SETADDITIONALCARETSVISIBLE = 2608; public const int SCI_GETADDITIONALCARETSVISIBLE = 2609; public const int SCI_GETSELECTIONS = 2570; public const int SCI_GETSELECTIONEMPTY = 2650; public const int SCI_CLEARSELECTIONS = 2571; public const int SCI_SETSELECTION = 2572; public const int SCI_ADDSELECTION = 2573; public const int SCI_SELECTIONFROMPOINT = 2474; public const int SCI_DROPSELECTIONN = 2671; public const int SCI_SETMAINSELECTION = 2574; public const int SCI_GETMAINSELECTION = 2575; public const int SCI_SETSELECTIONNCARET = 2576; public const int SCI_GETSELECTIONNCARET = 2577; public const int SCI_SETSELECTIONNANCHOR = 2578; public const int SCI_GETSELECTIONNANCHOR = 2579; public const int SCI_SETSELECTIONNCARETVIRTUALSPACE = 2580; public const int SCI_GETSELECTIONNCARETVIRTUALSPACE = 2581; public const int SCI_SETSELECTIONNANCHORVIRTUALSPACE = 2582; public const int SCI_GETSELECTIONNANCHORVIRTUALSPACE = 2583; public const int SCI_SETSELECTIONNSTART = 2584; public const int SCI_GETSELECTIONNSTART = 2585; public const int SCI_SETSELECTIONNEND = 2586; public const int SCI_GETSELECTIONNEND = 2587; public const int SCI_SETRECTANGULARSELECTIONCARET = 2588; public const int SCI_GETRECTANGULARSELECTIONCARET = 2589; public const int SCI_SETRECTANGULARSELECTIONANCHOR = 2590; public const int SCI_GETRECTANGULARSELECTIONANCHOR = 2591; public const int SCI_SETRECTANGULARSELECTIONCARETVIRTUALSPACE = 2592; public const int SCI_GETRECTANGULARSELECTIONCARETVIRTUALSPACE = 2593; public const int SCI_SETRECTANGULARSELECTIONANCHORVIRTUALSPACE = 2594; public const int SCI_GETRECTANGULARSELECTIONANCHORVIRTUALSPACE = 2595; public const int SCVS_NONE = 0; public const int SCVS_RECTANGULARSELECTION = 1; public const int SCVS_USERACCESSIBLE = 2; public const int SCVS_NOWRAPLINESTART = 4; public const int SCI_SETVIRTUALSPACEOPTIONS = 2596; public const int SCI_GETVIRTUALSPACEOPTIONS = 2597; public const int SCI_SETRECTANGULARSELECTIONMODIFIER = 2598; public const int SCI_GETRECTANGULARSELECTIONMODIFIER = 2599; public const int SCI_SETADDITIONALSELFORE = 2600; public const int SCI_SETADDITIONALSELBACK = 2601; public const int SCI_SETADDITIONALSELALPHA = 2602; public const int SCI_GETADDITIONALSELALPHA = 2603; public const int SCI_SETADDITIONALCARETFORE = 2604; public const int SCI_GETADDITIONALCARETFORE = 2605; public const int SCI_ROTATESELECTION = 2606; public const int SCI_SWAPMAINANCHORCARET = 2607; public const int SCI_MULTIPLESELECTADDNEXT = 2688; public const int SCI_MULTIPLESELECTADDEACH = 2689; public const int SCI_CHANGELEXERSTATE = 2617; public const int SCI_CONTRACTEDFOLDNEXT = 2618; public const int SCI_VERTICALCENTRECARET = 2619; public const int SCI_MOVESELECTEDLINESUP = 2620; public const int SCI_MOVESELECTEDLINESDOWN = 2621; public const int SCI_SETIDENTIFIER = 2622; public const int SCI_GETIDENTIFIER = 2623; public const int SCI_RGBAIMAGESETWIDTH = 2624; public const int SCI_RGBAIMAGESETHEIGHT = 2625; public const int SCI_RGBAIMAGESETSCALE = 2651; public const int SCI_MARKERDEFINERGBAIMAGE = 2626; public const int SCI_REGISTERRGBAIMAGE = 2627; public const int SCI_SCROLLTOSTART = 2628; public const int SCI_SCROLLTOEND = 2629; public const int SC_TECHNOLOGY_DEFAULT = 0; public const int SC_TECHNOLOGY_DIRECTWRITE = 1; public const int SC_TECHNOLOGY_DIRECTWRITERETAIN = 2; public const int SC_TECHNOLOGY_DIRECTWRITEDC = 3; public const int SC_TECHNOLOGY_DIRECT_WRITE_1 = 4; public const int SCI_SETTECHNOLOGY = 2630; public const int SCI_GETTECHNOLOGY = 2631; public const int SCI_CREATELOADER = 2632; public const int SCI_FINDINDICATORSHOW = 2640; public const int SCI_FINDINDICATORFLASH = 2641; public const int SCI_FINDINDICATORHIDE = 2642; public const int SCI_VCHOMEDISPLAY = 2652; public const int SCI_VCHOMEDISPLAYEXTEND = 2653; public const int SCI_GETCARETLINEVISIBLEALWAYS = 2654; public const int SCI_SETCARETLINEVISIBLEALWAYS = 2655; public const int SC_LINE_END_TYPE_DEFAULT = 0; public const int SC_LINE_END_TYPE_UNICODE = 1; public const int SCI_SETLINEENDTYPESALLOWED = 2656; public const int SCI_GETLINEENDTYPESALLOWED = 2657; public const int SCI_GETLINEENDTYPESACTIVE = 2658; public const int SCI_SETREPRESENTATION = 2665; public const int SCI_GETREPRESENTATION = 2666; public const int SCI_CLEARREPRESENTATION = 2667; public const int SCI_CLEARALLREPRESENTATIONS = 2770; public const int SC_REPRESENTATION_PLAIN = 0; public const int SC_REPRESENTATION_BLOB = 1; public const int SC_REPRESENTATION_COLOUR = 0x10; public const int SCI_SETREPRESENTATIONAPPEARANCE = 2766; public const int SCI_GETREPRESENTATIONAPPEARANCE = 2767; public const int SCI_SETREPRESENTATIONCOLOUR = 2768; public const int SCI_GETREPRESENTATIONCOLOUR = 2769; public const int SCI_EOLANNOTATIONSETTEXT = 2740; public const int SCI_EOLANNOTATIONGETTEXT = 2741; public const int SCI_EOLANNOTATIONSETSTYLE = 2742; public const int SCI_EOLANNOTATIONGETSTYLE = 2743; public const int SCI_EOLANNOTATIONCLEARALL = 2744; public const int EOLANNOTATION_HIDDEN = 0x0; public const int EOLANNOTATION_STANDARD = 0x1; public const int EOLANNOTATION_BOXED = 0x2; public const int EOLANNOTATION_STADIUM = 0x100; public const int EOLANNOTATION_FLAT_CIRCLE = 0x101; public const int EOLANNOTATION_ANGLE_CIRCLE = 0x102; public const int EOLANNOTATION_CIRCLE_FLAT = 0x110; public const int EOLANNOTATION_FLATS = 0x111; public const int EOLANNOTATION_ANGLE_FLAT = 0x112; public const int EOLANNOTATION_CIRCLE_ANGLE = 0x120; public const int EOLANNOTATION_FLAT_ANGLE = 0x121; public const int EOLANNOTATION_ANGLES = 0x122; public const int SCI_EOLANNOTATIONSETVISIBLE = 2745; public const int SCI_EOLANNOTATIONGETVISIBLE = 2746; public const int SCI_EOLANNOTATIONSETSTYLEOFFSET = 2747; public const int SCI_EOLANNOTATIONGETSTYLEOFFSET = 2748; public const int SC_SUPPORTS_LINE_DRAWS_FINAL = 0; public const int SC_SUPPORTS_PIXEL_DIVISIONS = 1; public const int SC_SUPPORTS_FRACTIONAL_STROKE_WIDTH = 2; public const int SC_SUPPORTS_TRANSLUCENT_STROKE = 3; public const int SC_SUPPORTS_PIXEL_MODIFICATION = 4; public const int SC_SUPPORTS_THREAD_SAFE_MEASURE_WIDTHS = 5; public const int SCI_SUPPORTSFEATURE = 2750; public const int SC_LINECHARACTERINDEX_NONE = 0; public const int SC_LINECHARACTERINDEX_UTF32 = 1; public const int SC_LINECHARACTERINDEX_UTF16 = 2; public const int SCI_GETLINECHARACTERINDEX = 2710; public const int SCI_ALLOCATELINECHARACTERINDEX = 2711; public const int SCI_RELEASELINECHARACTERINDEX = 2712; public const int SCI_LINEFROMINDEXPOSITION = 2713; public const int SCI_INDEXPOSITIONFROMLINE = 2714; public const int SCI_STARTRECORD = 3001; public const int SCI_STOPRECORD = 3002; public const int SCI_SETILEXER = 4033; public const int SCI_GETLEXER = 4002; public const int SCI_COLOURISE = 4003; public const int SCI_SETPROPERTY = 4004; public const int KEYWORDSET_MAX = 8; public const int SCI_SETKEYWORDS = 4005; public const int SCI_GETPROPERTY = 4008; public const int SCI_GETPROPERTYEXPANDED = 4009; public const int SCI_GETPROPERTYINT = 4010; public const int SCI_GETLEXERLANGUAGE = 4012; public const int SCI_PRIVATELEXERCALL = 4013; public const int SCI_PROPERTYNAMES = 4014; public const int SC_TYPE_BOOLEAN = 0; public const int SC_TYPE_INTEGER = 1; public const int SC_TYPE_STRING = 2; public const int SCI_PROPERTYTYPE = 4015; public const int SCI_DESCRIBEPROPERTY = 4016; public const int SCI_DESCRIBEKEYWORDSETS = 4017; public const int SCI_GETLINEENDTYPESSUPPORTED = 4018; public const int SCI_ALLOCATESUBSTYLES = 4020; public const int SCI_GETSUBSTYLESSTART = 4021; public const int SCI_GETSUBSTYLESLENGTH = 4022; public const int SCI_GETSTYLEFROMSUBSTYLE = 4027; public const int SCI_GETPRIMARYSTYLEFROMSTYLE = 4028; public const int SCI_FREESUBSTYLES = 4023; public const int SCI_SETIDENTIFIERS = 4024; public const int SCI_DISTANCETOSECONDARYSTYLES = 4025; public const int SCI_GETSUBSTYLEBASES = 4026; public const int SCI_GETNAMEDSTYLES = 4029; public const int SCI_NAMEOFSTYLE = 4030; public const int SCI_TAGSOFSTYLE = 4031; public const int SCI_DESCRIPTIONOFSTYLE = 4032; [Flags] public enum MOD { SC_MOD_INSERTTEXT = 0x1, SC_MOD_DELETETEXT = 0x2, SC_MOD_CHANGESTYLE = 0x4, SC_MOD_CHANGEFOLD = 0x8, SC_PERFORMED_USER = 0x10, SC_PERFORMED_UNDO = 0x20, SC_PERFORMED_REDO = 0x40, SC_MULTISTEPUNDOREDO = 0x80, SC_LASTSTEPINUNDOREDO = 0x100, SC_MOD_CHANGEMARKER = 0x200, SC_MOD_BEFOREINSERT = 0x400, SC_MOD_BEFOREDELETE = 0x800, SC_MULTILINEUNDOREDO = 0x1000, SC_STARTACTION = 0x2000, SC_MOD_CHANGEINDICATOR = 0x4000, SC_MOD_CHANGELINESTATE = 0x8000, SC_MOD_CHANGEMARGIN = 0x10000, SC_MOD_CHANGEANNOTATION = 0x20000, SC_MOD_CONTAINER = 0x40000, SC_MOD_LEXERSTATE = 0x80000, SC_MOD_INSERTCHECK = 0x100000, SC_MOD_CHANGETABSTOPS = 0x200000, SC_MOD_CHANGEEOLANNOTATION = 0x400000, SC_MODEVENTMASKALL = 0x7FFFFF, } [Flags] public enum UPDATE { SC_UPDATE_CONTENT=1, SC_UPDATE_SELECTION=2, SC_UPDATE_V_SCROLL=4, SC_UPDATE_H_SCROLL=8, } public const int SCEN_CHANGE = 768; public const int SCEN_SETFOCUS = 512; public const int SCEN_KILLFOCUS = 256; public const int SCK_DOWN = 300; public const int SCK_UP = 301; public const int SCK_LEFT = 302; public const int SCK_RIGHT = 303; public const int SCK_HOME = 304; public const int SCK_END = 305; public const int SCK_PRIOR = 306; public const int SCK_NEXT = 307; public const int SCK_DELETE = 308; public const int SCK_INSERT = 309; public const int SCK_ESCAPE = 7; public const int SCK_BACK = 8; public const int SCK_TAB = 9; public const int SCK_RETURN = 13; public const int SCK_ADD = 310; public const int SCK_SUBTRACT = 311; public const int SCK_DIVIDE = 312; public const int SCK_WIN = 313; public const int SCK_RWIN = 314; public const int SCK_MENU = 315; public const int SCMOD_NORM = 0; public const int SCMOD_SHIFT = 1; public const int SCMOD_CTRL = 2; public const int SCMOD_ALT = 4; public const int SCMOD_SUPER = 8; public const int SCMOD_META = 16; public const int SC_AC_FILLUP = 1; public const int SC_AC_DOUBLECLICK = 2; public const int SC_AC_TAB = 3; public const int SC_AC_NEWLINE = 4; public const int SC_AC_COMMAND = 5; public const int SC_AC_SINGLE_CHOICE = 6; public const int SC_CHARACTERSOURCE_DIRECT_INPUT = 0; public const int SC_CHARACTERSOURCE_TENTATIVE_INPUT = 1; public const int SC_CHARACTERSOURCE_IME_RESULT = 2; public enum NOTIF { SCN_STYLENEEDED = 2000, SCN_CHARADDED = 2001, SCN_SAVEPOINTREACHED = 2002, SCN_SAVEPOINTLEFT = 2003, SCN_MODIFYATTEMPTRO = 2004, SCN_KEY = 2005, SCN_DOUBLECLICK = 2006, SCN_UPDATEUI = 2007, SCN_MODIFIED = 2008, SCN_MACRORECORD = 2009, SCN_MARGINCLICK = 2010, SCN_NEEDSHOWN = 2011, SCN_PAINTED = 2013, SCN_USERLISTSELECTION = 2014, SCN_URIDROPPED = 2015, SCN_DWELLSTART = 2016, SCN_DWELLEND = 2017, SCN_ZOOM = 2018, SCN_HOTSPOTCLICK = 2019, SCN_HOTSPOTDOUBLECLICK = 2020, SCN_CALLTIPCLICK = 2021, SCN_AUTOCSELECTION = 2022, SCN_INDICATORCLICK = 2023, SCN_INDICATORRELEASE = 2024, SCN_AUTOCCANCELLED = 2025, SCN_AUTOCCHARDELETED = 2026, SCN_HOTSPOTRELEASECLICK = 2027, SCN_FOCUSIN = 2028, SCN_FOCUSOUT = 2029, SCN_AUTOCCOMPLETED = 2030, SCN_MARGINRIGHTCLICK = 2031, SCN_AUTOCSELECTIONCHANGE = 2032, } public const int SC_BIDIRECTIONAL_DISABLED = 0; public const int SC_BIDIRECTIONAL_L2R = 1; public const int SC_BIDIRECTIONAL_R2L = 2; public const int SCI_GETBIDIRECTIONAL = 2708; public const int SCI_SETBIDIRECTIONAL = 2709; public struct Sci_CharacterRange { public int cpMin; public int cpMax; } public struct Sci_TextRange { public int cpMin; public int cpMax; public byte* lpstrText; } public struct Sci_TextToFind { public int cpMin; public int cpMax; public byte* lpstrText; public Sci_CharacterRange chrgText; } public struct Sci_Rectangle { public int left; public int top; public int right; public int bottom; } public struct Sci_RangeToFormat { public IntPtr hdc; public IntPtr hdcTarget; public Sci_Rectangle rc; public Sci_Rectangle rcPage; public Sci_CharacterRange chrg; } public struct Sci_NotifyHeader { public wnd hwndFrom; public nint idFrom; public NOTIF code; } public struct SCNotification { #pragma warning disable 649 //field never assigned public Sci_NotifyHeader nmhdr; /// Returns nmhdr.code. public NOTIF code => nmhdr.code; nint _position; /// Raw UTF-8 position. public int position => (int)_position; public int ch; public int modifiers; public MOD modificationType; public byte* textUTF8; nint _length; public int length => (int)_length; nint _linesAdded; public int linesAdded => (int)_linesAdded; public int message; public nint wParam; public nint lParam; nint _line; public int line => (int)_line; public int foldLevelNow; public int foldLevelPrev; public int margin; public int listType; public int x; public int y; public int token; nint _annotationLinesAdded; public int annotationLinesAdded => (int)_annotationLinesAdded; public UPDATE updated; public int listCompletionMethod; #pragma warning restore 649 //field never assigned /// /// Returns position, UTF-8. If SCN_MODIFIED(SC_MOD_INSERTTEXT|SC_MOD_BEFOREINSERT|SC_MOD_INSERTCHECK), adds length, because position then is old position. /// public int FinalPosition { get { int r = position; if (length > 0 && nmhdr.code == NOTIF.SCN_MODIFIED && modificationType.HasAny(MOD.SC_MOD_INSERTTEXT | MOD.SC_MOD_BEFOREINSERT | MOD.SC_MOD_INSERTCHECK) ) r += length; return r; } } /// /// Converts textUTF8 to C# string. /// Returns null if textUTF8 is null. /// Don't call this property multiple times for the same notification. Store the return value in a variable and use it. /// public string Text { get { if (textUTF8 == null) return null; if (textUTF8[0] == 0) return ""; return new string((sbyte*)textUTF8, 0, length, Encoding.UTF8); } } } } ================================================ FILE: Au.Controls/KScintilla/Sci adapter.cs ================================================ namespace Au.Controls; using static Sci; public unsafe partial class KScintilla { _Adapter _adapter; //created in KScintilla ctor /// /// Gets or sets text. /// Uses caching, therefore the get function is fast and garbage-free when calling multiple times. /// /// /// The get function gets cached text if called not the first time after setting or modifying control text. /// The set function calls when need. Uses default parameters (with undo and notifications, unless AaInitReadOnlyAlways). /// Unlike the above methods, this property can be used before creating handle. /// public string aaaText { get => _adapter.Text; set { _adapter.Text = value; } } /// /// UTF-8 text length. /// public int aaaLen8 => _adapter.Len8; /// /// UTF-16 text length. /// public int aaaLen16 => _adapter.Len16; /// /// Converts UTF-16 position to UTF-8 position. Fast. /// /// Negative or greater than . public int aaaPos8(int pos16) => _adapter.Pos8(pos16); /// /// Converts UTF-8 position to UTF-16 position. Fast. /// /// Negative or greater than . public unsafe int aaaPos16(int pos8) => _adapter.Pos16(pos8); /// /// Maps UTF-16 to/from UTF-8 positions. /// Gets UTF-16 and UTF-8 length. /// Gets cached UTF-16 text. /// class _Adapter { record struct _NA(int i8, int i16, int len16, int charLen); readonly KScintilla _sci; string _text; readonly List<_NA> _a; int _len8, _len16; bool _mapDone; public _Adapter(KScintilla sci) { _sci = sci; _a = new(); } public void HandleCreated() { if (!_text.NE()) _sci.aaaSetText(_text, SciSetTextFlags.NoUndoNoNotify); } public void TextModified() { _text = null; _a.Clear(); _mapDone = false; } string _GetText() => _sci._RangeText(0, Len8); public string Text { get { //if (_sci.Name == "document") print.qm2.write($"Text: cached={_text != null}"); if (_text == null && !_sci._w.Is0) _text = _GetText(); //_NotifyModified sets _text=null return _text; } set { if (_sci._w.Is0) _text = value; //will set control text on WM_CREATE else _sci.aaaSetText(value); //_NotifyModified sets _text=null. Control text can be != value, eg when tags parsed. } } public int Len8 => _mapDone ? _len8 : _sci.Call(SCI_GETTEXTLENGTH); public int Len16 { get { if (_text != null) return _text.Length; if (!_mapDone) _CreatePosMap(); return _len16; } } public int Pos8(int pos16) { if (!_mapDone) _CreatePosMap(); Debug.Assert((uint)pos16 <= _len16); if ((uint)pos16 > _len16) throw new ArgumentOutOfRangeException(nameof(pos16), $"pos16 = {pos16}, _len16 = {_len16}, _GetText().Length={_GetText().Length}"); //using binary search find max _a[r].i16 that is < pos16 int r = -1, from = 0, to = _a.Count; while (to > from) { int m = (from + to) / 2; if (_a[m].i16 < pos16) from = (r = m) + 1; else to = m; } if (r < 0) return pos16; //_a is empty (ASCII text) or pos16 <= _a[0].i16 (before first non-ASCII character) var p = _a[r]; return p.i8 + Math.Min(pos16 - p.i16, p.len16) * p.charLen + Math.Max(pos16 - (p.i16 + p.len16), 0); //p.i8 + utf + ascii } public unsafe int Pos16(int pos8) { if (!_mapDone) _CreatePosMap(); Debug.Assert((uint)pos8 <= _len8); if ((uint)pos8 > _len8) throw new ArgumentOutOfRangeException(nameof(pos8), $"pos8 = {pos8}, _len8 = {_len8}, _sci.Call(SCI_GETTEXTLENGTH)={_sci.Call(SCI_GETTEXTLENGTH)}"); //using binary search find max _a[r].i8 that is < pos8 int r = -1, from = 0, to = _a.Count; while (to > from) { int m = (from + to) / 2; if (_a[m].i8 < pos8) from = (r = m) + 1; else to = m; } if (r < 0) return pos8; //_a is empty (ASCII text) or pos8 <= _a[0].i8 (before first non-ASCII character) var p = _a[r]; int len8 = p.len16 * p.charLen; return p.i16 + Math.Min(pos8 - p.i8, len8) / p.charLen + Math.Max(pos8 - (p.i8 + len8), 0); //p.i16 + utf + ascii } [MethodImpl(MethodImplOptions.AggressiveOptimization)] unsafe void _CreatePosMap() { //This func is fast and often garbageless. For code edit controls don't need to optimize to avoid calling it frequently, eg for each added character. //Should not be used for output/log controls if called on each "append text". int textLen; int gap = Sci_Range(_sci.AaSciPtr, 0, -1, out var p, out var p2, &textLen); int to8 = p2 == null ? textLen : gap; int i8 = 0, i16 = 0; for (; ; ) { //ASCII range int start8 = i8; int lenAscii8 = new RByte(p + i8, to8 - i8).IndexOfAnyExceptInRange((byte)0, (byte)127); if (lenAscii8 < 0) i8 = to8; else i8 += lenAscii8; i16 += i8 - start8; //non-ASCII range if (i8 < to8) { start8 = i8; int charLen = 0; while (i8 < to8 && p[i8] >= 0x80) { Rune.DecodeFromUtf8(new(p + i8, to8 - i8), out _, out int nb); if (charLen == 0) charLen = nb; else if (nb != charLen) break; i8 += nb; } if (charLen == 4) charLen = 2; int len16 = (i8 - start8) / charLen; _a.Add(new(start8, i16, len16, charLen)); i16 += len16; if (i8 < to8) continue; } //the part after gap if (p2 == null) break; p = p2 - i8; p2 = null; to8 = textLen; } _len8 = textLen; _len16 = i16; _mapDone = true; } } } ================================================ FILE: Au.Controls/KScintilla/Sci loader.cs ================================================ namespace Au.Controls; using static Sci; public unsafe partial class KScintilla { public class aaaFileLoaderSaver { _Encoding _enc; byte[] _text; public bool IsBinary => _enc == _Encoding.Binary; public bool IsImage { get; private set; } /// /// Loads file as UTF-8. /// Supports any encoding (UTF-8, UTF-16, etc), BOM. Remembers it for Save. /// /// Exceptions of File.OpenRead, File.Read, Encoding.Convert. public void Load(string file) { _enc = _Encoding.Binary; IsImage = false; if (file.Ends(true, ".png", ".bmp", ".jpg", ".jpeg", ".gif", ".tif", ".tiff", ".ico", ".cur", ".ani") > 0) { if (!filesystem.exists(file).File) throw new FileNotFoundException($"Could not find file '{file}'."); IsImage = true; _text = Encoding.UTF8.GetBytes($""); return; } using var fr = filesystem.loadStream(file); if (fr.Length > 100_000_000) { _text = "//Cannot edit. The file is too big, more than 100_000_000 bytes."u8.ToArray(); return; } int fileSize = (int)fr.Length; var b = new byte[fileSize]; if (fr.Read(b, 0, fileSize) != fileSize) throw null; _enc = _DetectEncoding(b); //print.it(_enc); if (_enc == _Encoding.Binary) { _text = "//Cannot edit. The file is binary, not text."u8.ToArray(); return; } if (_enc == _Encoding.Utf8_BOM && !System.Text.Unicode.Utf8.IsValid(b[3..])) b = b.ToStringUTF8().ToUTF8(); //else Scintilla text would be bad if (_EncodingEnumToObject() is { } e) { int bomLength = (int)_enc >>> 4; b = Encoding.Convert(e, Encoding.UTF8, b, bomLength, (int)fileSize - bomLength); } _text = b; } Encoding _EncodingEnumToObject() { switch (_enc) { case _Encoding.Utf16_BOM or _Encoding.Utf16: return Encoding.Unicode; case _Encoding.Utf16BE_BOM or _Encoding.Utf16BE: return Encoding.BigEndianUnicode; case _Encoding.Utf32_BOM: return Encoding.UTF32; case _Encoding.Utf32BE_BOM: return new UTF32Encoding(true, false); case _Encoding.Ansi: return StringUtil.GetEncoding(-1); } return null; } static unsafe _Encoding _DetectEncoding(RByte s) { int len = s.Length; //is too short to have a BOM? if (len == 0) return _Encoding.Utf8; if (len == 1) return s[0] == 0 ? _Encoding.Binary : (s[0] < 128 ? _Encoding.Utf8 : _Encoding.Ansi); //has a BOM? if (s is [0xEF, 0xBB, 0xBF, ..]) return _Encoding.Utf8_BOM; if (s is [0xFF, 0xFE, 0, 0, ..]) return _Encoding.Utf32_BOM; if (s is [0xFF, 0xFE, ..]) return _Encoding.Utf16_BOM; if (s is [0xFE, 0xFF, ..]) return _Encoding.Utf16BE_BOM; if (s is [0, 0, 0xFE, 0xFF, ..]) return _Encoding.Utf32BE_BOM; //has '\0'? int zeroAt = s.IndexOf((byte)0); if (zeroAt is 0 or 1 && 0 == (len & 1)) { if (MemoryMarshal.Cast(s).Contains('\0')) return _Encoding.Binary; return zeroAt is 0 ? _Encoding.Utf16BE : _Encoding.Utf16; } //if (zeroAt == len - 1) { s = s[..--len]; zeroAt = -1; } //WordPad saves .rtf files with '\0' at the end. Rejected; eg VS and VSCode detects as binary. if (zeroAt >= 0) return _Encoding.Binary; return System.Text.Unicode.Utf8.IsValid(s) ? _Encoding.Utf8 : _Encoding.Ansi; } enum _Encoding : byte { /// Not a text file, or loading failed, or not initialized. Binary = 0, //must be 0 /// ASCII or UTF-8 without BOM. Utf8 = 1, /// UTF-8 with BOM (3 bytes). Utf8_BOM = 1 | (3 << 4), /// ANSI containing non-ASCII characters, unknown code page. Ansi = 2, /// UTF-16 without BOM. Utf16 = 3, /// UTF-16 big endian without BOM. Utf16BE = 4, /// UTF-16 with BOM (2 bytes). Utf16_BOM = 3 | (2 << 4), /// UTF-16 with big endian BOM (2 bytes). Utf16BE_BOM = 4 | (2 << 4), /// UTF-32 with BOM (4 bytes). Utf32_BOM = 5 | (4 << 4), /// UTF-32 with big endian BOM (4 bytes). Utf32BE_BOM = 6 | (4 << 4), //rejected. .NET does not save/load with UTF-7 BOM, so we too. Several different BOM of different length. ///// UTF-7 with BOM. //Utf7_BOM, } /// /// Sets control text. /// If the file is image, binary or too big (> 100_000_000), sets to display the image or/and some short info text, makes the control read-only, sets Save to throw exception, and returns false. Else returns true. /// Uses NoUndo and NoNotify. /// Must be called once. /// public unsafe bool SetText(KScintilla k) { RByte text = _text; if (_enc == _Encoding.Utf8_BOM) text = text[3..]; if (_enc == _Encoding.Binary) k.Call(SCI_SETREADONLY); //caller may set AaInitReadOnlyAlways = true using (new _NoUndoNotif(k, SciSetTextFlags.NoUndoNoNotify)) { if (IsImage && k.AaTags is { } tags) tags.AddText(text.ToStringUTF8(), false, false, dontHideImageTag: true); else k.aaaSetString(SCI_APPENDTEXT, text.Length, text); } if (_enc != _Encoding.Binary) return true; k.Call(SCI_SETREADONLY, 1); return false; } /// /// Returns true if text contains newlines other than \r\n and \n. /// public bool DetectBadNewlines() { if (_enc != _Encoding.Binary) { var s = _text; for (int i = 0; i < s.Length - 1;) { //ends with '\0' switch (s[i++]) { case 13 when s[i] != 10: return true; case 0xc2 when s[i] == 0x85: return true; case 0xe2 when s[i] == 0x80 && s[i + 1] is 0xa8 or 0xa9: return true; } } } return false; } public void FinishedLoading() { _text = null; //GC } /// /// Saves control text with the same encoding/BOM as loaded. Uses . /// /// To pass to filesystem.save. /// To pass to filesystem.save. /// Exceptions of filesystem.save. /// The file is binary (then SetText made the control read-only), or Load not called. public unsafe void Save(KScintilla k, string file, string tempDirectory = null) { if (_enc == _Encoding.Binary) throw new InvalidOperationException(); //_enc = _Encoding.; //test Encoding e = _EncodingEnumToObject(); int bom = (int)_enc >> 4; //BOM length uint bomm = 0; //BOM memory if (e != null) bomm = _enc switch { _Encoding.Utf16_BOM or _Encoding.Utf32_BOM => 0xFEFF, _Encoding.Utf16BE_BOM => 0xFFFE, _Encoding.Utf32BE_BOM => 0xFFFE0000, _ => 0 }; else if (bom == 3) bomm = 0xBFBBEF; //UTF8; else bom 0 //print.it(_enc, bom, bomm, e); filesystem.save(file, temp => { using var fs = File.Create(temp); if (bomm != 0) { uint u = bomm; fs.Write(new RByte((byte*)&u, bom)); } //rare if (e != null) { //rare var bytes = e.GetBytes(k.aaaText); //convert encoding. aaaText likely gets cached text, fast fs.Write(bytes); } else { int len = k.aaaLen8; var bytes = (byte*)k.CallRetPtr(SCI_GETCHARACTERPOINTER); fs.Write(new RByte(bytes, len)); } }, tempDirectory: tempDirectory); //print.it("file", File.ReadAllBytes(file)); } } } ================================================ FILE: Au.Controls/KScintilla/Sci other.cs ================================================ namespace Au.Controls; using static Sci; public partial class KScintilla { #region markers /// /// Sets marker style and colors. /// /// Marker index, 0-31. Indices 25-31 are used for folding markers (SC_MARKNUM_FOLDERx). Indices 21-24 are used for change history markers. Scintilla draws markers from smaller to bigger index. /// SC_MARK_. /// SCI_MARKERSETFORE. /// SCI_MARKERSETBACK. public void aaaMarkerDefine(int marker, int style, ColorInt? foreColor = null, ColorInt? backColor = null) { if ((uint)marker > 31) throw new ArgumentOutOfRangeException(nameof(marker)); Call(SCI_MARKERDEFINE, marker, style); if (foreColor != null) Call(SCI_MARKERSETFORE, marker, foreColor.Value.ToBGR()); if (backColor != null) Call(SCI_MARKERSETBACK, marker, backColor.Value.ToBGR()); } /// /// SCI_MARKERADD. /// /// Marker handle, or -1 if failed. public int aaaMarkerAdd(int marker, int line) { return Call(SCI_MARKERADD, line, marker); } /// /// SCI_MARKERADD in line containing pos. /// /// Marker handle, or -1 if failed. public int aaaMarkerAdd(int marker, bool utf16, int pos) { return aaaMarkerAdd(marker, aaaLineFromPos(utf16, pos)); } /// /// SCI_MARKERDELETE. /// public void aaaMarkerDelete(int marker, int line) { Call(SCI_MARKERDELETE, line, marker); } /// /// SCI_MARKERDELETE in line containing pos. /// public void aaaMarkerDelete(int marker, bool utf16, int pos) { aaaMarkerDelete(marker, aaaLineFromPos(utf16, pos)); } /// /// SCI_MARKERDELETEALL. /// /// If -1, delete all markers from all lines. public void aaaMarkerDeleteAll(int marker) { Call(SCI_MARKERDELETEALL, marker); } /// /// SCI_MARKERDELETEHANDLE. /// public void aaaMarkerDeleteHandle(int handle) { Call(SCI_MARKERDELETEHANDLE, handle); } #endregion #region indicators /// /// Sets indicator style, color, etc. /// /// Indicator index, 0-31. Scintilla draws indicators from smaller to bigger index. /// Eg Sci.INDIC_FULLBOX. /// SCI_INDICSETFORE. /// SCI_INDICSETALPHA. Valid for some styles. /// SCI_INDICSETOUTLINEALPHA. Valid for some styles. /// SCI_INDICSETSTROKEWIDTH (%). Valid for some styles. /// SCI_INDICSETUNDER (under text). /// SCI_INDICSETHOVERFORE. /// SCI_INDICSETHOVERSTYLE public void aaaIndicatorDefine(int indic, int style, ColorInt? color = null, int? alpha = null, int? borderAlpha = null, int? strokeWidth = null, bool underText = false, ColorInt? hoverColor = null, int? hoverStyle = null) { if ((uint)indic > 31) throw new ArgumentOutOfRangeException(nameof(indic)); Call(SCI_INDICSETSTYLE, indic, style); if (style != INDIC_HIDDEN) { if (color != null) Call(SCI_INDICSETFORE, indic, color.Value.ToBGR()); if (alpha != null) Call(SCI_INDICSETALPHA, indic, alpha.Value); if (borderAlpha != null) Call(SCI_INDICSETOUTLINEALPHA, indic, borderAlpha.Value); if (strokeWidth != null) Call(SCI_INDICSETSTROKEWIDTH, indic, strokeWidth.Value); if (underText) Call(SCI_INDICSETUNDER, indic, underText); } if (hoverColor != null) Call(SCI_INDICSETHOVERFORE, indic, hoverColor.Value.ToBGR()); if (hoverStyle != null) Call(SCI_INDICSETHOVERSTYLE, indic, hoverStyle.Value); } public void aaaIndicatorClear(int indic) => aaaIndicatorClear(indic, false, ..); public void aaaIndicatorClear(int indic, bool utf16, Range r) { var (from, to) = aaaNormalizeRange(utf16, r); Call(SCI_SETINDICATORCURRENT, indic); Call(SCI_INDICATORCLEARRANGE, from, to - from); } public void aaaIndicatorAdd(int indic, bool utf16, Range r, int value = 1) { var (from, to) = aaaNormalizeRange(utf16, r); Call(SCI_SETINDICATORCURRENT, indic); Call(SCI_SETINDICATORVALUE, value); Call(SCI_INDICATORFILLRANGE, from, to - from); } /// /// SCI_INDICATORVALUEAT. /// public int aaaIndicatorGetValue(int indic, int pos, bool utf16 = false) { if (utf16) pos = aaaPos8(pos); return Call(SCI_INDICATORVALUEAT, indic, pos); } /// /// SCI_INDICATORALLONFOR. Returns a bitmap value representing which indicators are non-zero at pos. /// public int aaaIndicatorGetAll(int pos, bool utf16 = false) { if (utf16) pos = aaaPos8(pos); return Call(SCI_INDICATORALLONFOR, pos); } /// /// Finds the first range of the specified indicator. /// /// /// UTF-8. /// false if not found. public bool aaaIndicatorFindFirst(int indic, out StartEnd r) { r = default; int start = Call(SCI_INDICATOREND, indic); if (start <= 0) { if (start < 0 || aaaIndicatorGetValue(indic, 0) == 0) return false; } int end = Call(SCI_INDICATOREND, indic, start); if (end <= start) return false; r = new(start, end); return true; } #endregion #region margins /// /// SCI_SETMARGINTYPEN. Optionally SCI_SETMARGINSENSITIVEN, SCI_SETMARGINCURSORN, SCI_SETMARGINMASKN. /// /// /// SC_MARGIN_. /// SCI_SETMARGINMASKN. /// SCI_SETMARGINSENSITIVEN. /// SCI_SETMARGINCURSORN. True SC_CURSORARROW, false SC_CURSORREVERSEARROW. public void aaaMarginSetType(int margin, int type, int? markersMask = null, bool? sensitive = null, bool? cursorArrow = null) { Call(SCI_SETMARGINTYPEN, margin, type); if (markersMask.HasValue) Call(SCI_SETMARGINMASKN, margin, markersMask.Value); if (sensitive.HasValue) Call(SCI_SETMARGINSENSITIVEN, margin, sensitive.Value); if (cursorArrow.HasValue) Call(SCI_SETMARGINCURSORN, margin, cursorArrow == true ? SC_CURSORARROW : SC_CURSORREVERSEARROW); } /// /// SCI_SETMARGINWIDTHN or SCI_SETMARGINLEFT or SCI_SETMARGINRIGHT. /// Dpi-scales, and will scale when DPI changed. /// /// Margin index for SCI_SETMARGINWIDTHN, or -1 to use SCI_SETMARGINLEFT, or -2 to use SCI_SETMARGINRIGHT. /// Logical pixels or 0. /// Number of character '8' widths or 0. If both pixels and chars are not 0, uses their sum. public void aaaMarginSetWidth(int margin, int pixels, int chars = 0) { _marginDpi ??= new (short, short)[Call(SCI_GETMARGINS) + 2]; _marginDpi[margin + 2] = ((short)pixels, (short)chars); _MarginSetWidth(margin, pixels, chars); } (short pixels, short chars)[] _marginDpi; //logical pixels void _MarginWidthsDpiChanged() { if (_marginDpi is { } a) for (int i = a.Length; --i >= 0;) if (a[i] != default) _MarginSetWidth(i - 2, a[i].pixels, a[i].chars); } void _MarginSetWidth(int i, int pixels, int chars) { if (pixels != 0) pixels = Dpi.Scale(pixels, _dpi); if (chars != 0) pixels += chars * aaaStyleMeasureStringWidth(STYLE_LINENUMBER, "8"); if (i == -2) Call(SCI_SETMARGINRIGHT, 0, pixels); else if (i == -1) Call(SCI_SETMARGINLEFT, 0, pixels); else Call(SCI_SETMARGINWIDTHN, i, pixels); } /// Margin index, or -1 if not in a margin. public int aaaMarginFromPoint(POINT p, bool screenCoord = false) { if (screenCoord) _w.MapScreenToClient(ref p); if (_w.ClientRect.Contains(p)) { for (int i = 0, n = Call(SCI_GETMARGINS), w = 0; i < n; i++) { w += Call(SCI_GETMARGINWIDTHN, i); if (w >= p.x) return i; } } return -1; } /// /// SCI_GETMARGINWIDTHN. /// public (int left, int right) aaaMarginGetX(int margin, bool dpiUnscale = false) { int x = 0; for (int i = 0; i < margin; i++) x += Call(SCI_GETMARGINWIDTHN, i); var r = (left: x, right: x + Call(SCI_GETMARGINWIDTHN, margin)); if (dpiUnscale) { r.left = Dpi.Unscale(r.left, _dpi).ToInt(); r.right = Dpi.Unscale(r.right, _dpi).ToInt(); } return r; } /// /// Initializes folding margin and optionally separator marker. /// /// Margin index. /// Separator marker index, or -1 if never using seperators in this control. /// Set SC_AUTOMATICFOLD_CLICK. public void aaaFoldingInit(int foldMargin = 0, int separatorMarker = -1, bool autoFold = false) { Call(SCI_SETMARGINTYPEN, foldMargin, SC_MARGIN_SYMBOL); Call(SCI_SETMARGINMASKN, foldMargin, SC_MASK_FOLDERS); Call(SCI_SETMARGINSENSITIVEN, foldMargin, 1); Call(SCI_MARKERDEFINE, SC_MARKNUM_FOLDEROPEN, SC_MARK_BOXMINUS); Call(SCI_MARKERDEFINE, SC_MARKNUM_FOLDER, SC_MARK_BOXPLUS); Call(SCI_MARKERDEFINE, SC_MARKNUM_FOLDERSUB, SC_MARK_VLINE); Call(SCI_MARKERDEFINE, SC_MARKNUM_FOLDERTAIL, SC_MARK_LCORNER); Call(SCI_MARKERDEFINE, SC_MARKNUM_FOLDEREND, SC_MARK_BOXPLUSCONNECTED); Call(SCI_MARKERDEFINE, SC_MARKNUM_FOLDEROPENMID, SC_MARK_BOXMINUSCONNECTED); Call(SCI_MARKERDEFINE, SC_MARKNUM_FOLDERMIDTAIL, SC_MARK_TCORNER); for (int i = 25; i < 32; i++) { Call(SCI_MARKERSETFORE, i, 0xffffff); Call(SCI_MARKERSETBACK, i, 0x808080); Call(SCI_MARKERSETBACKSELECTED, i, i == SC_MARKNUM_FOLDER ? 0xFF : 0x808080); } //Call(SCI_MARKERENABLEHIGHLIGHT, 1); //red [+] int fflags = SC_AUTOMATICFOLD_SHOW //show hidden lines when header line deleted. Also when hidden text modified, and it is not always good. | SC_AUTOMATICFOLD_CHANGE; //show hidden lines when header line modified like '#region' -> '//#region' if (autoFold) fflags |= SC_AUTOMATICFOLD_CLICK; Call(SCI_SETAUTOMATICFOLD, fflags); Call(SCI_SETFOLDFLAGS, SC_FOLDFLAG_LINEAFTER_CONTRACTED); Call(SCI_FOLDDISPLAYTEXTSETSTYLE, SC_FOLDDISPLAYTEXT_STANDARD); aaaStyleForeColor(STYLE_FOLDDISPLAYTEXT, 0x808080); Call(SCI_SETMARGINCURSORN, foldMargin, SC_CURSORARROW); aaaMarginSetWidth(foldMargin, 12); //separator lines below functions, types etc if (separatorMarker >= 0) aaaMarkerDefine(separatorMarker, SC_MARK_UNDERLINE, backColor: 0xa0a0a0); } /// /// Adds or updates fold points and optionally separators. /// /// Fold points. If null or empty, just clears old. /// Separator marker index, or -1 if never using seperators in this control. The marker should have an underline style. public void aaaFoldingApply(List af, int separatorMarker = -1) { int underlinedLine = 0; int[] a = null; if (af != null) { a = new int[af.Count]; for (int i = 0; i < a.Length; i++) { var v = af[i]; int pos8 = aaaPos8(v.pos); a[i] = pos8 | (v.start ? 0 : unchecked((int)0x80000000)); if (separatorMarker >= 0 && v.separator != 0) { //add separator below, or above if start if (v.start) { //above int k = v.pos - v.separator; if (k <= 0) continue; pos8 = aaaPos8(k); } int li = aaaLineFromPos(false, pos8); _DeleteUnderlinedLineMarkers(li); //if(underlinedLine != li) print.it("add", li + 1); if (underlinedLine != li) Call(SCI_MARKERADD, li, separatorMarker); else underlinedLine++; } } } if (separatorMarker >= 0) _DeleteUnderlinedLineMarkers(int.MaxValue); void _DeleteUnderlinedLineMarkers(int beforeLine) { if ((uint)underlinedLine > beforeLine) return; int marker = 1 << separatorMarker; for (; ; underlinedLine++) { underlinedLine = Call(SCI_MARKERNEXT, underlinedLine, marker); if ((uint)underlinedLine >= beforeLine) break; //print.it("delete", underlinedLine + 1); do Call(SCI_MARKERDELETE, underlinedLine, separatorMarker); while (0 != (marker & Call(SCI_MARKERGET, underlinedLine))); } } unsafe { //we implement folding in Scintilla. Calling many SCI_SETFOLDLEVEL here would be slow. fixed (int* ip = a) Sci_SetFoldLevels(AaSciPtr, 0, -1, a.Lenn_(), ip); } //p1.NW('F'); } /// /// SCI_GETFOLDLEVEL. /// /// 0-based level (never less than 0) and whether it has SC_FOLDLEVELHEADERFLAG. public (int level, bool isHeader) aaaFoldingLevel(int line) { int r = Call(SCI_GETFOLDLEVEL, line); return (Math.Max(0, (r & SC_FOLDLEVELNUMBERMASK) - SC_FOLDLEVELBASE), 0 != (r & SC_FOLDLEVELHEADERFLAG)); } #endregion } /// /// See /// /// A position anywhere in the line. /// Start or end. /// If not 0, adds separator. If start true, adds above, at pos-separator; else adds below, and let it be 1. public record struct SciFoldPoint(int pos, bool start, ushort separator = 0); ================================================ FILE: Au.Controls/KScintilla/Sci styles.cs ================================================ namespace Au.Controls; using static Sci; public unsafe partial class KScintilla { public void aaaStyleFont(int style, string name) { aaaSetString(SCI_STYLESETFONT, style, name); } //public string aaaStyleFont(int style) //{ // return aaaGetString(SCI_STYLEGETFONT, style, 100); //} public void aaaStyleFont(int style, string name, double size) { aaaStyleFont(style, name); aaaStyleFontSize(style, size); } /// Uses only font name and size. Not style etc. public void aaaStyleFont(int style, System.Windows.Controls.Control c) { aaaStyleFont(style, c.FontFamily.ToString(), c.FontSize.ToInt() * 72 / 96); } /// Segoe UI, 9. public void aaaStyleFont(int style) { aaaStyleFont(style, "Segoe UI", 9); } public void aaaStyleFontSize(int style, double value) { Call(SCI_STYLESETSIZEFRACTIONAL, style, (int)(value * 100)); } //public int aaaStyleFontSize(int style) //{ // return Call(SCI_STYLEGETSIZE, style); //} public void aaaStyleHidden(int style, bool value) { Call(SCI_STYLESETVISIBLE, style, !value); } //public bool aaaStyleHidden(int style) //{ // return 0 == Call(SCI_STYLEGETVISIBLE, style); //} public void aaaStyleBold(int style, bool value) { Call(SCI_STYLESETBOLD, style, value); } public void aaaStyleItalic(int style, bool value) { Call(SCI_STYLESETITALIC, style, value); } public void aaaStyleUnderline(int style, bool value) { Call(SCI_STYLESETUNDERLINE, style, value); } public void aaaStyleEolFilled(int style, bool value) { Call(SCI_STYLESETEOLFILLED, style, value); } public void aaaStyleHotspot(int style, bool value) { Call(SCI_STYLESETHOTSPOT, style, value); } public bool aaaStyleHotspot(int style) { return 0 != Call(SCI_STYLEGETHOTSPOT, style); } public void aaaStyleForeColor(int style, ColorInt color) { Call(SCI_STYLESETFORE, style, color.ToBGR()); } public void aaaStyleBackColor(int style, ColorInt color) { Call(SCI_STYLESETBACK, style, color.ToBGR()); } /// /// SCI_TEXTWIDTH. /// public int aaaStyleMeasureStringWidth(int style, string s) { return aaaSetString(SCI_TEXTWIDTH, style, s); } /// /// Calls SCI_STYLECLEARALL, which sets all styles to be the same as STYLE_DEFAULT. /// Then also sets some special styles: STYLE_HIDDEN, SC_ELEMENT_HOT_SPOT_ACTIVE. /// /// Clear only styles 0..STYLE_DEFAULT. public void aaaStyleClearAll(bool belowDefault = false) { if (belowDefault) aaaStyleClearRange(0, STYLE_DEFAULT); else Call(SCI_STYLECLEARALL); aaaStyleHidden(STYLE_HIDDEN, true); //aaaSetString(SCI_STYLESETINVISIBLEREPRESENTATION, STYLE_HIDDEN, "-"u8); //no aaaSetElementColor(SC_ELEMENT_HOT_SPOT_ACTIVE, 0x8000FF); } /// /// Calls SCI_STYLECLEARALL(styleFrom8, styleTo8NotIncluding), which sets range of styles to be the same as STYLE_DEFAULT. /// If styleTo8NotIncluding is 0, clears all starting from styleFrom. /// public void aaaStyleClearRange(int styleFrom8, int styleTo8NotIncluding = 0) { Call(SCI_STYLECLEARALL, styleFrom8, styleTo8NotIncluding); } /// /// Gets style at position. /// Uses SCI_GETSTYLEINDEXAT. /// Returns 0 if pos is invalid. /// public int aaaStyleGetAt(int pos8) { return Call(SCI_GETSTYLEINDEXAT, pos8); } /// /// Sets scintilla's "end-styled position" = to8 (default int.MaxValue), to avoid SCN_STYLENEEDED notifications. /// Fast, just sets a field in scintilla. /// /// /// Scintilla sends SCN_STYLENEEDED, unless a lexer is set. In some cases 1 or several, in some cases many, in some cases every 500 ms. /// public void aaaSetStyled(int to8 = int.MaxValue) => Call(SCI_STARTSTYLING, to8); /// /// SCI_SETELEMENTCOLOUR. /// /// SC_ELEMENT_. /// Color. Can be with alpha. If null, calls SCI_RESETELEMENTCOLOUR. public void aaaSetElementColor(int element, ColorInt? color) { if (color.HasValue) Call(SCI_SETELEMENTCOLOUR, element, color.Value.ToBGR(zeroAlpha: false)); else Call(SCI_RESETELEMENTCOLOUR, element); } /// /// SCI_GETELEMENTCOLOUR. /// /// SC_ELEMENT_. public ColorInt aaaGetElementColor(int element) { return ColorInt.FromBGR(Call(SCI_GETELEMENTCOLOUR, element), false); } /// /// SCI_STARTSTYLING and SCI_SETSTYLINGEX. /// public void aaaSetStyling(int start8, RByte styles) { Call(SCI_STARTSTYLING, start8); unsafe { fixed (byte* bp = styles) Call(SCI_SETSTYLINGEX, styles.Length, bp); } } /// /// If text is ASCII, returns styles. Else returns new array where style bytes are converted from UTF-16 offsets to UTF-8 offsets. /// /// Style bytes. The offsets are UTF-16. The function does not modify it. /// Text to be styled. Must be same length as styles. /// public static Span aaaConvertStylingBytesToUtf8(Span styles, RStr text) { Debug.Assert(styles.Length == text.Length); if (!text.IsAscii()) { var u = new byte[Encoding.UTF8.GetByteCount(text)]; int i = 0, j = 0; foreach (var r in text.EnumerateRunes()) { var v = styles[i++]; if (!r.IsBmp) i++; for (int n8 = r.Utf8SequenceLength; n8-- > 0;) u[j++] = v; } styles = u; } return styles; } } ================================================ FILE: Au.Controls/KScintilla/Sci text.cs ================================================ //Functions to work with Scintilla control text, etc. namespace Au.Controls; using static Sci; public unsafe partial class KScintilla { #region low level /// /// Calls a Scintilla message that sets a string which is passed using lParam. /// The string can be null if the Scintilla message allows it. /// If the message changes control text, this function does not work if the control is read-only. At first make non-readonly temporarily. /// Don't call this function from another thread. /// public int aaaSetString(int sciMessage, nint wParam, RStr lParam) { fixed (byte* s = _ToUtf8(lParam, out var len)) { return Call(sciMessage, wParam, s); } } /// /// Calls a Scintilla message that sets a string which is passed using lParam (UTF-8 string) and wParam (UTF-8 string length). /// The string can be null if the Scintilla message allows it. /// If the message changes control text, this function does not work if the control is read-only. At first make non-readonly temporarily. /// Don't call this function from another thread. /// public int aaaSetString(int sciMessage, RStr lParam) { fixed (byte* s = _ToUtf8(lParam, out var len)) { return Call(sciMessage, len, s); } } /// /// Calls a Scintilla message that sets a string which is passed using wParam. /// The string can be null if the Scintilla message allows it. /// If the message changes control text, this function does not work if the control is read-only. At first make non-readonly temporarily. /// Don't call this function from another thread. /// public int aaaSetString(int sciMessage, RStr wParam, nint lParam) { fixed (byte* s = _ToUtf8(wParam)) { return Call(sciMessage, (nint)s, lParam); } } /// /// Calls a Scintilla message that sets a string which is passed using lParam. /// With many messages the lParam string must be '\0'-terminated, eg UTF-8 string literal like "example"u8. /// The string can be null if the Scintilla message allows it. /// If the message changes control text, this function does not work if the control is read-only. At first make non-readonly temporarily. /// public int aaaSetString(int sciMessage, nint wParam, RByte lParam) { fixed (byte* p = lParam) { return Call(sciMessage, wParam, p); } } /// /// Calls a Scintilla message and passes two strings using wParam and lParam. /// wParamlParam must be like "WPARAM\0LPARAM". Asserts if no '\0'. /// If the message changes control text, this function does not work if the control is read-only. At first make non-readonly temporarily. /// Don't call this function from another thread. /// public int aaaSetStringString(int sciMessage, RStr wParamlParam) { fixed (byte* s = _ToUtf8(wParamlParam, out var len)) { int i = Ptr_.Length(s); Debug.Assert(i < len); return Call(sciMessage, (nint)s, s + i + 1); } } /// /// Calls a Scintilla message that gets a string when length is known. /// Always uses utf8Length bytes of the result (does not find length). /// Can get binary string (with '\0' characters). /// /// /// /// /// Known length (bytes) of the result UTF-8 string, without the terminating '\0' character. /// If 0, returns "" and does not call the message. /// public string aaaGetStringOfLength(int sciMessage, nint wParam, int utf8Length) => _GetString(sciMessage, wParam, utf8Length, false); /// /// Calls a Scintilla message that gets a string. See . /// To get buffer size, at first calls sciMessage with lParam=0 (null buffer). /// Can get binary string (with '\0' characters). /// Don't call this function from another thread. /// /// /// public string aaaGetStringGetLength(int sciMessage, nint wParam) => _GetString(sciMessage, wParam, Call(sciMessage, wParam), false); /// /// Calls a Scintilla message that gets a '\0'-terminated string. /// Cannot get binary string (with '\0' characters). /// Don't call this function from another thread. /// /// /// /// /// How much UTF-8 bytes to allocate for Scintilla to store the text. /// Can be either known or max expected text length, without the terminating '\0' character. The function will find length of the retrieved string (finds '\0'). /// If 0, returns "" and does not call the message. /// public string aaaGetString0Terminated(int sciMessage, nint wParam, int bufferSize) => _GetString(sciMessage, wParam, bufferSize, true); [SkipLocalsInit] string _GetString(int sciMessage, nint wParam, int len, bool findLength) { if (len == 0) return ""; using FastBuffer b = new(len + 1); b[len] = 0; Call(sciMessage, wParam, b.p); Debug.Assert(b[len] == 0); if (findLength) len = b.FindByteStringLength(); return Encoding.UTF8.GetString(b, len); } static string _FromUtf8(byte* b) => Convert2.Utf8Decode(b); static byte[] _ToUtf8(RStr s) => Convert2.Utf8Encode(s); static byte[] _ToUtf8(RStr s, out int utf8Length) { var r = Convert2.Utf8Encode(s); utf8Length = r.Length - 1; return r; } /// /// Optimized 'get text' function. /// /// Start index, UTF-8. /// End index, UTF-8. /// /// Does not create an intermediate byte[]. /// Gets big text 5 times faster than aaaGetStringOfLength. Tested with text 31K length, 1K lines. /// string _RangeText(int start8, int end8) { Debug.Assert(end8 >= start8); Debug.Assert((uint)end8 <= aaaLen8); if (end8 == start8) return ""; int gap = Sci_Range(AaSciPtr, start8, end8, out var p1, out var p2); if (p2 != null) { int n1 = gap - start8, n2 = end8 - gap; int len1 = Encoding.UTF8.GetCharCount(p1, n1); int len2 = Encoding.UTF8.GetCharCount(p2, n2); nint k1 = (nint)p1, k2 = (nint)p2; return string.Create(len1 + len2, (k1, k2, n1, n2), static (span, a) => { int len1 = Encoding.UTF8.GetChars(new RByte((byte*)a.k1, a.n1), span); Encoding.UTF8.GetChars(new RByte((byte*)a.k2, a.n2), span.Slice(len1)); }); } else { int n1 = end8 - start8; int len1 = Encoding.UTF8.GetCharCount(p1, n1); nint k1 = (nint)p1; return string.Create(len1, (k1, n1), static (span, a) => { Encoding.UTF8.GetChars(new RByte((byte*)a.k1, a.n1), span); }); } } /// /// If utf16, converts from and to from characters to UTF-8 bytes. /// /// Input values are UTF-16. /// /// If -1, uses . /// Invalid argument, eg greater than text length or to less than from. public void aaaNormalizeRange(bool utf16, ref int from, ref int to) { if (from < 0 || (to < from && to != -1)) throw new ArgumentOutOfRangeException(); if (utf16) from = aaaPos8(from); if (to < 0) to = aaaLen8; else if (utf16) to = aaaPos8(to); } /// /// If utf16, converts from and to from characters to UTF-8 bytes. /// /// Input values are UTF-16. /// Range. Can be spacified from start or/and from end. /// Invalid argument, eg to less than from. public (int from, int to) aaaNormalizeRange(bool utf16, Range r) { int from, to; if (r.Start.IsFromEnd || r.End.IsFromEnd) { (from, to) = r.GetStartEnd(utf16 ? aaaLen16 : aaaLen8); if (utf16) { from = aaaPos8(from); to = aaaPos8(to); } } else { from = r.Start.Value; to = r.End.Value; aaaNormalizeRange(utf16, ref from, ref to); } return (from, to); } /// /// Same as , but can be to less than from. If so, returns true. /// /// Invalid argument, eg greater than text length. public bool aaaNormalizeRangeCanBeReverse(bool utf16, ref int from, ref int to, bool swapFromTo) { bool reverse = to >= 0 && to < from; if (reverse) Math2.Swap(ref from, ref to); aaaNormalizeRange(utf16, ref from, ref to); if (reverse && !swapFromTo) Math2.Swap(ref from, ref to); return reverse; } /// /// => utf16 ? aaaPos8(pos) : pos; /// /// Negative. int _ParamPos(bool utf16, int pos) => pos >= 0 ? (utf16 ? aaaPos8(pos) : pos) : throw new ArgumentOutOfRangeException(); /// /// => utf16 ? aaaPos16(pos) : pos; /// /// Negative. int _ReturnPos(bool utf16, int pos) => pos >= 0 ? (utf16 ? aaaPos16(pos) : pos) : throw new ArgumentOutOfRangeException(); /// /// pos >= 0 ? (utf16 ? aaaPos16(pos) : pos) : pos; /// int _ReturnPosCanBeNegative(bool utf16, int pos) => pos >= 0 ? (utf16 ? aaaPos16(pos) : pos) : pos; /// /// => line; /// /// Negative. int _ParamLine(int line) => line >= 0 ? line : throw new ArgumentOutOfRangeException(); struct _NoReadonly : IDisposable { KScintilla _t; bool _ro; public _NoReadonly(KScintilla t) { _t = t; _ro = _t.AaInitReadOnlyAlways || _t.aaaIsReadonly; if (_ro) _t.Call(SCI_SETREADONLY, 0); } public void Dispose() { if (_ro) _t.Call(SCI_SETREADONLY, 1); } } struct _NoUndoNotif : IDisposable { KScintilla _t; bool _noUndo, _noNotif; public _NoUndoNotif(KScintilla t, SciSetTextFlags flags) { _t = t; _noUndo = flags.Has(SciSetTextFlags.NoUndo) && 0 != _t.Call(SCI_GETUNDOCOLLECTION); _noNotif = flags.Has(SciSetTextFlags.NoNotify) && !_t.AaDisableModifiedNotifications; if (_noNotif) _t.AaDisableModifiedNotifications = true; if (_noUndo) _t.Call(SCI_SETUNDOCOLLECTION); } public void Dispose() { if (_noUndo) { _t.Call(SCI_EMPTYUNDOBUFFER); _t.Call(SCI_SETUNDOCOLLECTION, 1); } if (_noNotif) _t.AaDisableModifiedNotifications = false; } } #endregion #region set/get/clear all text, append text /// /// Removes all text (SCI_CLEARALL). /// /// public void aaaClearText(SciSetTextFlags flags = 0) { if (_w.Is0) return; using (new _NoUndoNotif(this, flags)) using (new _NoReadonly(this)) Call(SCI_CLEARALL); } /// /// Replaces all text. /// Parses tags if need. /// /// Text. /// /// Don't parse tags, regardless of AaInitTagsStyle. public void aaaSetText(string s, SciSetTextFlags flags = 0, bool ignoreTags = false) { using (new _NoUndoNotif(this, flags)) { if (!ignoreTags && _CanParseTags(s)) { aaaClearText(); AaTags.AddText(s, false, AaInitTagsStyle == AaTagsStyle.AutoWithPrefix); } else { using (new _NoReadonly(this)) aaaSetString(SCI_SETTEXT, 0, s ?? ""); } } } bool _CanParseTags(string s) { if (s.NE()) return false; return AaInitTagsStyle switch { AaTagsStyle.AutoAlways => s.Contains('<'), AaTagsStyle.AutoWithPrefix => s.Starts("<>"), _ => false, }; } /// /// Appends text and optionally "\r\n". /// Parses tags if need. Optionally scrolls and moves current position to the end (SCI_GOTOPOS). /// /// /// Also append "\r\n". Ignores (uses true) if parses tags. /// Move current position and scroll to the end. /// Don't parse tags, regardless of AaInitTagsStyle. public void aaaAppendText(string s, bool andRN, bool scroll, bool ignoreTags = false) { s ??= ""; if (!ignoreTags && _CanParseTags(s)) { AaTags.AddText(s, true, AaInitTagsStyle == AaTagsStyle.AutoWithPrefix, scroll); } else { var a = Convert2.Utf8Encode(s, andRN ? "\r\n" : ""); using (new _NoReadonly(this)) fixed (byte* b = a) Call(SCI_APPENDTEXT, a.Length, b); if (scroll) Call(SCI_GOTOPOS, aaaLen8); } } /// /// SCI_APPENDTEXT. /// /// /// Move current position and scroll to the end. public void aaaAppendText8(RByte s, bool scroll) { using (new _NoReadonly(this)) { fixed (byte* p = s) Call(SCI_APPENDTEXT, s.Length, p); } if (scroll) Call(SCI_GOTOPOS, aaaLen8); } /// /// Sets or appends UTF-8 text of specified length. Does not parse tags. /// If scroll, moves current position and scrolls to the end (SCI_GOTOPOS). /// internal void aaaAddText8_(bool append, bool scroll, byte* s, int lenToAppend) { using (new _NoReadonly(this)) if (append) Call(SCI_APPENDTEXT, lenToAppend, s); else Call(SCI_SETTEXT, 0, s); if (scroll) Call(SCI_GOTOPOS, aaaLen8); } //not used now ///// ///// Sets or appends styled UTF-8 text of specified length. ///// Does not append newline (s should contain it). Does not parse tags. Moves current position and scrolls to the end. ///// Uses SCI_ADDSTYLEDTEXT. Caller does not have to move cursor to the end. ///// lenToAppend is length in bytes, not in cells. ///// //internal void aaaAddStyledText_(bool append, byte* s, int lenBytes) //{ // if(append) Call(SCI_SETEMPTYSELECTION, TextLengthBytes); // using(new _NoReadonly(this)) // if(!append) Call(SCI_SETTEXT); // Call(SCI_ADDSTYLEDTEXT, lenBytes, s); // if(append) Call(SCI_GOTOPOS, TextLengthBytes); //} #endregion /// /// Gets (SCI_GETCURRENTPOS) or sets (SCI_SETEMPTYSELECTION) current caret position in UTF-8 bytes. /// The set function makes empty selection; does not scroll and does not make visible like aaaGoToPos. /// public int aaaCurrentPos8 { get => Call(SCI_GETCURRENTPOS); set => Call(SCI_SETEMPTYSELECTION, value); } /// /// Gets (SCI_GETCURRENTPOS) or sets (SCI_SETEMPTYSELECTION) current caret position in UTF-16 chars. /// The set function makes empty selection; does not scroll and does not make visible like aaaGoToPos. /// public int aaaCurrentPos16 { get => aaaPos16(aaaCurrentPos8); set => Call(SCI_SETEMPTYSELECTION, aaaPos8(value)); } /// /// SCI_GETSELECTIONSTART UTF-8. /// public int aaaSelectionStart8 => Call(SCI_GETSELECTIONSTART); /// /// SCI_GETSELECTIONSTART UTF-16. /// public int aaaSelectionStart16 => aaaPos16(aaaSelectionStart8); /// /// SCI_GETSELECTIONEND UTF-8. /// Always greater or equal than SelectionStart8. /// public int aaaSelectionEnd8 => Call(SCI_GETSELECTIONEND); /// /// SCI_GETSELECTIONEND UTF-16. /// Always greater or equal than SelectionStart16. /// public int aaaSelectionEnd16 => aaaPos16(aaaSelectionEnd8); /// /// utf16 ? (aaaSelectionStart16, aaaSelectionEnd16) : (aaaSelectionStart8, aaaSelectionEnd8) /// public (int start, int end) aaaSelection(bool utf16) => utf16 ? (aaaSelectionStart16, aaaSelectionEnd16) : (aaaSelectionStart8, aaaSelectionEnd8); /// /// true if !SCI_GETSELECTIONEMPTY. /// public bool aaaHasSelection => 0 == Call(SCI_GETSELECTIONEMPTY); /// /// Gets line index from character position. /// /// /// A position in document text. Returns the last line if too big. public int aaaLineFromPos(bool utf16, int pos) => Call(SCI_LINEFROMPOSITION, _ParamPos(utf16, pos)); /// /// Gets line index at . /// public int aaaLineFromPos() => Call(SCI_LINEFROMPOSITION, aaaSelectionStart8); /// /// Gets line start position from line index. /// /// Return UTF-16. /// 0-based line index. Returns text length if too big. public int aaaLineStart(bool utf16, int line) => _ReturnPos(utf16, _LineStart(line)); int _LineStart(int line) { if (line < 0) throw new ArgumentOutOfRangeException(); int R = Call(SCI_POSITIONFROMLINE, _ParamLine(line)); return R >= 0 ? R : aaaLen8; //If line < 0, Scintilla returns line start from selection start. //If line > number of lines, Scintilla returns -1. } /// /// Gets line end position from line index. /// /// Return UTF-16. /// 0-based line index. Returns text length if too big. /// Include \r\n. public int aaaLineEnd(bool utf16, int line, bool withRN = false) { line = _ParamLine(line); return _ReturnPos(utf16, withRN ? _LineStart(line + 1) : Call(SCI_GETLINEENDPOSITION, line)); } /// /// Gets line start position from any position. /// /// pos is UTF-16. Return UTF-16. /// A position in document text. Returns text length if too big. public int aaaLineStartFromPos(bool utf16, int pos) => aaaLineStart(utf16, aaaLineFromPos(utf16, pos)); /// /// Gets line start position from any position and gets line index. /// Returns start position. /// /// pos is UTF-16. Return UTF-16. /// A position in document text. Returns text length if too big. /// Receives line index. public int aaaLineStartFromPos(bool utf16, int pos, out int line) => aaaLineStart(utf16, line = aaaLineFromPos(utf16, pos)); /// /// Gets line end position from any position. /// /// pos is UTF-16. Return UTF-16. /// A position in document text. Returns text length if too big. /// Include \r\n. /// If pos is at a line start (0 or after '\n' character), return pos. public int aaaLineEndFromPos(bool utf16, int pos, bool withRN = false, bool lineStartIsLineEnd = false) { int pos0 = pos; pos = _ParamPos(utf16, pos); if (lineStartIsLineEnd) { if (pos == 0 || aaaCharAt8(pos - 1) == '\n') return pos0; } return aaaLineEnd(utf16, aaaLineFromPos(false, pos), withRN); } /// /// Gets line index, start and end positions from position. /// /// pos is UTF-16. Return UTF-16. /// A position in document text. Uses the last line if too big. /// Include \r\n. /// If not null, overrides utf16 for return values. public (int line, int start, int end) aaaLineStartEndFromPos(bool utf16, int pos, bool withRN = false, bool? utf16Return = null) { int startPos = aaaLineStartFromPos(false, _ParamPos(utf16, pos), out int line); int endPos = aaaLineEnd(false, line, withRN); utf16 = utf16Return ?? utf16; return (line, _ReturnPos(utf16, startPos), _ReturnPos(utf16, endPos)); } /// /// Gets line text. /// /// 0-based line index. If invalid, returns "". /// Include \r\n. public string aaaLineText(int line, bool withRN = false) => _RangeText(aaaLineStart(false, line), aaaLineEnd(false, line, withRN)); /// /// Gets line height. /// Currently all lines are of the same height. /// public int aaaLineHeight() => Call(SCI_TEXTHEIGHT, 0); /// /// Gets the number of lines. /// public int aaaLineCount => Call(SCI_GETLINECOUNT); /// /// Gets the number of tabs + spaces/4 at the start of the line that contains the specified position. /// /// /// A position in document text. /// Receives the number of extra spaces, 0 to 3. public int aaaLineIndentFromPos(bool utf16, int pos, out int extraSpaces) { int line = aaaLineFromPos(utf16, pos); int i = Call(SCI_GETLINEINDENTATION, line), r = i / 4; extraSpaces = i - r * 4; return r; } /// /// Gets the number of tabs + spaces/4 at the start of the line that contains the specified position. /// /// /// A position in document text. public int aaaLineIndentFromPos(bool utf16, int pos) => aaaLineIndentFromPos(utf16, pos, out _); /// /// Gets position from point. /// /// Return UTF-16. /// Point in client area. /// Return -1 if p is not in text characters. public int aaaPosFromXY(bool utf16, POINT p, bool minusOneIfFar) => _ReturnPosCanBeNegative(utf16, Call(minusOneIfFar ? SCI_POSITIONFROMPOINTCLOSE : SCI_POSITIONFROMPOINT, p.x, p.y)); /// /// Gets annotation text of line. /// Returns "" if the line does not contain annotation or is invalid line index. /// public string aaaAnnotationText(int line) => AaImages?.AnnotationText_(line) ?? aaaAnnotationText_(line); /// /// Gets raw annotation text which can contain image info. /// aaaAnnotationText gets text without image info. /// Returns "" if the line does not contain annotation or is invalid line index. /// public string aaaAnnotationText_(int line) => aaaGetStringGetLength(SCI_ANNOTATIONGETTEXT, line); /// /// Sets annotation text of line. /// Does nothing if invalid line index. /// If s is null or "", removes annotation. /// Preserves existing image info. /// public void aaaAnnotationText(int line, string s, bool eol = false) { if (eol) aaaAnnotationText_(line, s, eol); else if (AaImages != null) AaImages.AnnotationText_(line, s); else aaaAnnotationText_(line, s); } /// /// Sets raw annotation text which can contain image info. /// If s is null or "", removes annotation. /// internal void aaaAnnotationText_(int line, string s, bool eol = false) { if (s.NE()) s = null; aaaSetString(eol ? SCI_EOLANNOTATIONSETTEXT : SCI_ANNOTATIONSETTEXT, line, s); } /// /// Moves from to the start of its line, and to to the end of its line. /// Does not change to if it is at a line start. /// /// /// Start index. /// End index. /// Include "\r\n". public void aaaRangeToFullLines(bool utf16, ref int from, ref int to, bool withRN = false) { Debug.Assert(from <= to); from = _ReturnPos(utf16, aaaLineStartFromPos(utf16, from)); to = _ReturnPos(utf16, aaaLineEndFromPos(utf16, to, withRN, true)); } /// /// SCI_INSERTTEXT. /// /// /// Start index. Cannot be negative. /// Text to insert. Can be null. /// Call before. /// Call after. /// If pos is hidden because of folding, finally collapse its folding again. See . /// /// Does not parse tags. /// Does not change current selection, unless pos is in it; for it use or . /// public void aaaInsertText(bool utf16, int pos, string s, bool addUndoPointBefore = false, bool addUndoPointAfter = false, bool restoreFolding = false) { if (addUndoPointBefore) aaaAddUndoPoint(); using (new _NoReadonly(this)) using (new aaaFoldingRestorer(restoreFolding ? this : null, pos)) aaaSetString(SCI_INSERTTEXT, _ParamPos(utf16, pos), s ?? ""); if (addUndoPointAfter) aaaAddUndoPoint(); } /// /// If ctor detects that the line from pos is hidden because of folding, Dispose collapses its folding again. /// Use when modifying text to prevent unfolding. /// public struct aaaFoldingRestorer : IDisposable { KScintilla _sci; int _foldLine; //tested: temp setting SCI_SETAUTOMATICFOLD does not work. If restoring async, does not expand, but draws incorrectly. /// Can be null, then does nothing. /// public aaaFoldingRestorer(KScintilla sci, int pos) { _sci = sci; _foldLine = -1; if (sci != null) { int line = sci.aaaLineFromPos(true, pos); if (0 == sci.Call(SCI_GETLINEVISIBLE, line)) _foldLine = sci.Call(SCI_GETFOLDPARENT, line); } } public void Dispose() { if (_foldLine < 0) return; _sci.Call(SCI_FOLDLINE, _foldLine); //If at the modified line index was a nested folding point, Scintilla will expand again, very async. // Could restore again with the following code, but it can be dangerous, eg document closed. Never mind. //var sci = _sci; var i = _foldLine; //timer.after(300, _ => sci.Call(SCI_FOLDLINE, i)); } } ///// ///// Inserts text at current position. ///// Does not parse tags. ///// Does not change current selection; for it use . ///// ///// Text to insert. Can be null. //public void aaaInsertText(string s) //{ // using(new _NoReadonly(this)) // aaaSetString(SCI_INSERTTEXT, -1, s ?? ""); //} /// /// SCI_DELETERANGE. /// /// /// Start index. /// End index. If -1, uses control text length. /// /// Does not parse tags. /// Does not change current selection, unless it is in the range (including to); for it use or . /// public void aaaDeleteRange(bool utf16, int from, int to) { aaaNormalizeRange(utf16, ref from, ref to); using (new _NoReadonly(this)) Call(SCI_DELETERANGE, from, to - from); } /// /// Replaces text range. /// /// /// Start index. /// End index. If -1, uses control text length. Can be less than from. /// Replacement text. Can be null. /// /// After replacing set curent position at the end of the replacement. If from less than to - at from. /// Else if current position was in the range (including to), Scintilla sets at from. /// Else does not change current position and selection. /// /// /// Does not parse tags. /// By default does not change current selection, unless it is in the range (including to). /// public void aaaReplaceRange(bool utf16, int from, int to, string s, bool moveCurrentPos = false) { bool reverse = aaaNormalizeRangeCanBeReverse(utf16, ref from, ref to, swapFromTo: true); using (new _NoReadonly(this)) { int fromEnd = !moveCurrentPos || reverse ? 0 : aaaLen8 - to; Call(SCI_SETTARGETRANGE, from, to); aaaSetString(SCI_REPLACETARGET, s ?? ""); if (moveCurrentPos) aaaCurrentPos8 = reverse ? from : aaaLen8 - fromEnd; } } /// /// Gets range text. /// /// /// Start index. /// End index. If -1, uses control text length. public string aaaRangeText(bool utf16, int from, int to) { aaaNormalizeRange(utf16, ref from, ref to); return _RangeText(from, to); } /// /// Gets direct pointer to a text range in Scintilla buffer (SCI_GETRANGEPOINTER). /// Does not validate arguments, just asserts to >= from. /// /// UTF-8 start position. /// UTF-8 end position. public byte* aaaRangePointer(int from, int to) { Debug.Assert(to >= from); return (byte*)CallRetPtr(SCI_GETRANGEPOINTER, from, to - from); } /// /// Gets direct pointer to a text range in Scintilla buffer (SCI_GETRANGEPOINTER). /// Does not validate arguments, just asserts to >= from. /// /// UTF-8 start position. /// UTF-8 end position. public RByte aaaRangeSpan(int from, int to) => new(aaaRangePointer(from, to), to - from); /// /// SCI_REPLACESEL. /// /// Replacement text. Can be null. /// /// Does not parse tags. /// If read-only, asserts and fails (unlike most other functions that change text). /// public void aaaReplaceSel(string s) { Debug.Assert(!aaaIsReadonly); aaaSetString(SCI_REPLACESEL, 0, s ?? ""); } /// /// Sets selection (SCI_SETSEL) and replaces with new text (SCI_REPLACESEL). /// /// /// Start index. /// End index. If -1, uses control text length. Can be less than from. /// Replacement text. Can be null. /// /// Does not parse tags. /// If read-only, asserts and fails (unlike most other functions that change text). /// public void aaaSetAndReplaceSel(bool utf16, int from, int to, string s) { Debug.Assert(!aaaIsReadonly); aaaSelect(utf16, from, to); aaaSetString(SCI_REPLACESEL, 0, s ?? ""); } /// /// SCI_GOTOPOS and ensures visible. /// public void aaaGoToPos(bool utf16, int pos) { pos = _ParamPos(utf16, pos); int line = Call(SCI_LINEFROMPOSITION, pos); Call(SCI_ENSUREVISIBLEENFORCEPOLICY, line); Call(SCI_GOTOPOS, pos); } /// /// SCI_GOTOLINE and ensures visible. /// public void aaaGoToLine(int line) { Call(SCI_ENSUREVISIBLEENFORCEPOLICY, line); Call(SCI_GOTOLINE, line); } /// /// SCI_SETSEL and optionally ensures visible. /// /// /// /// If -1, uses text length. Else to can be less than from. Caret will be at to. /// Ensure line visible and selection visible. Without it in some cases selection to the left of the caret may be invisible. public void aaaSelect(bool utf16, int from, int to, bool makeVisible = false) { aaaNormalizeRangeCanBeReverse(utf16, ref from, ref to, swapFromTo: false); if (makeVisible) aaaGoToPos(false, from); Call(SCI_SETSEL, from, to); } /// /// SCI_GETREADONLY, SCI_SETREADONLY. /// public bool aaaIsReadonly { get => 0 != Call(SCI_GETREADONLY); set => Call(SCI_SETREADONLY, value ? 1 : 0); } //public bool aaaIsReadonly { // get => _isReadOnly; // set { // if (value != _isReadOnly) { // _isReadOnly = value; // if (!_w.Is0) Call(SCI_SETREADONLY, _isReadOnly); // } // } //} //bool _isReadOnly; /// /// Gets text and offsets of lines containing selection. /// Returns true. If ifFullLines is true, may return false. /// /// Return UTF-16. /// Results. /// Fail (return false) if selection length is 0 or selection start is not at a line start. /// Get +1 line if selection ends at a line start, except if selection length is 0. public bool aaaGetSelectionLines(bool utf16, out (int selStart, int selEnd, int linesStart, int linesEnd, string text) x, bool ifFullLines = false, bool oneMore = false) { x = default; x.selStart = aaaSelectionStart8; x.selEnd = aaaSelectionEnd8; if (ifFullLines && x.selEnd == x.selStart) return false; var (_, start, end) = aaaLineStartEndFromPos(false, x.selStart); if (ifFullLines && start != x.selStart) return false; x.linesStart = start; if (x.selEnd > x.selStart) { (_, start, end) = aaaLineStartEndFromPos(false, x.selEnd); if (!oneMore && start == x.selEnd) end = start; //selection end is at line start. We need the line only if oneMore. if (ifFullLines && x.selEnd < end) return false; } x.linesEnd = end; x.text = _RangeText(x.linesStart, end); if (utf16) { x.linesStart = aaaPos16(x.linesStart); x.linesEnd = aaaPos16(x.linesEnd); x.selStart = aaaPos16(x.selStart); x.selEnd = aaaPos16(x.selEnd); } return true; } public string aaaSelectedText() => _RangeText(aaaSelectionStart8, aaaSelectionEnd8); /// /// SCI_FINDTEXT. /// /// pos is UTF-16. Return UTF-16. /// /// /// If -1, text length. public unsafe int aaaFindText(bool utf16, RStr s, int start = 0, int end = -1) { aaaNormalizeRange(utf16, ref start, ref end); fixed (byte* b = _ToUtf8(s)) { var k = new Sci_TextToFind { cpMin = start, cpMax = end, lpstrText = b, chrgText = default }; return _ReturnPosCanBeNegative(utf16, Call(SCI_FINDTEXT, SCFIND_MATCHCASE, &k)); } //tested: with SCI_SEARCHINTARGET slightly slower } /// /// SCI_GETCHARAT. /// public char aaaCharAt8(int pos8) => (char)Call(SCI_GETCHARAT, pos8); /// /// SCI_BEGINUNDOACTION, SCI_ENDUNDOACTION. /// public void aaaAddUndoPoint() { Call(SCI_BEGINUNDOACTION); Call(SCI_ENDUNDOACTION); } /// /// SCI_BEGINUNDOACTION. /// /// Final counter of nested undo actions. Called SCI_BEGINUNDOACTION if it's 1. public int aaaBeginUndoAction() { if (_inUndoAction == 0) Call(SCI_BEGINUNDOACTION); return ++_inUndoAction; } int _inUndoAction; /// /// SCI_ENDUNDOACTION. /// /// Final counter of nested undo actions. Called SCI_ENDUNDOACTION if it's 0. public int aaaEndUndoAction() { Debug.Assert(_inUndoAction > 0); if (--_inUndoAction == 0) Call(SCI_ENDUNDOACTION); return _inUndoAction; } //currently not used. Moved to SciCode, which also supports onUndoDontChangeCaretPos. ///// ///// => new aaaUndoAction(this); ///// //public aaaUndoAction aaaNewUndoAction() => new aaaUndoAction(this); ///// ///// Ctor calls . Dispose() calls . ///// Does nothing if it's a nested undo action. ///// //public struct aaaUndoAction : IDisposable { // KScintilla _sci; // /// // /// Calls SCI_BEGINUNDOACTION. // /// // /// Can be null, then does nothing. // public aaaUndoAction(KScintilla sci) { // _sci = sci; // _sci?.aaaBeginUndoAction(); // } // /// // /// Calls SCI_ENDUNDOACTION and clears this variable. // /// // public void Dispose() { // if (_sci != null) { // _sci.aaaEndUndoAction(); // _sci = null; // } // } //} } /// /// Flags for 'set text', 'clear text' and similar functions. Eg you can disable Undo collection or 'changed' notifications. /// Note: Ignores NoUndo and NoNotify if AaInitReadOnlyAlways, because then Undo and notifications are disabled when creating control. /// [Flags] public enum SciSetTextFlags { /// /// Cannot be undone. Clear Undo buffer. /// NoUndo = 1, /// /// Don't send 'modified' and 'text changed' notifications (don't call overrides and events). /// NoNotify = 2, /// /// NoUndo | NoNotify. /// NoUndoNoNotify = 3, } /// /// Provides fast direct access to a range of UTF-8 characters in Scintilla internal text. /// Uses SCI_GETRANGEPOINTER. See . /// Ensures that the gap is not moved (it could be slow if frequently). /// unsafe struct SciDirectRange { int _from, _to, _gap; byte* _p1, _p2; //before and after gap public SciDirectRange(KScintilla sci, int from8, int to8) { _from = from8; _to = to8; _gap = sci.Call(SCI_GETGAPPOSITION); //print.it(_from, _to, _gap); if (_gap > _from && _gap < _to) { _p1 = sci.aaaRangePointer(_from, _gap); _p2 = sci.aaaRangePointer(_gap, _to); } else { _p1 = sci.aaaRangePointer(_from, _to); _p2 = null; } } /// /// Returns character at position i in entire text (not from the start of the range). /// /// public char this[int i] { get { if (i < _from || i >= _to) throw new IndexOutOfRangeException(); if (_p2 == null || i < _gap) return (char)_p1[i - _from]; return (char)_p2[i - _gap]; } } } ================================================ FILE: Au.Controls/KScintilla/SciImages.cs ================================================ namespace Au.Controls; using static Sci; /// /// Gets image file paths etc from control text and displays the images below that lines. /// /// /// Draws images in annotation areas. /// Supports text annotations too, below images and in no-image lines. But it is limited: /// 1. To set/get it use , not direct Scintilla API. /// 2. You cannot hide all annotations (SCI_ANNOTATIONSETVISIBLE). This class sets it to show always. /// 3. You cannot clear all annotations (SCI_ANNOTATIONCLEARALL). /// 4. Setting annotation styles is currently not supported. /// public unsafe class SciImages { class _Image { public long nameHash, evictionTime; public byte[] data; public int width, height; } class _ThreadSharedData { List<_Image> _a; int _dpi; timer _timer; public int CacheSize { get; private set; } public void AddImage(_Image im) { _a ??= []; _a.Add(im); CacheSize += im.data.Length; im.evictionTime = Environment.TickCount64 + 2000; //rejected: keep im in cache longer if the loading was slow. Eg AV scans exe files when we extract icons. // Usually only the first time is slow. Later the file is in the OS file cache. _timer ??= new(t => { var now = Environment.TickCount64; for (int i = _a.Count; --i >= 0;) if (now - _a[i].evictionTime > 0) { CacheSize -= _a[i].data.Length; _a.RemoveAt(i); } if (_a.Count == 0) _timer.Stop(); }); if (!_timer.IsRunning) _timer.Every(500); } public _Image FindImage(long nameHash, int dpi) { if (dpi != _dpi) { ClearCache(); _dpi = dpi; } if (!_a.NE_()) { for (int j = 0; j < _a.Count; j++) if (_a[j].nameHash == nameHash) return _a[j]; } return null; } public void ClearCache() { _a?.Clear(); CacheSize = 0; } /// /// If cache is large (at least MaxCacheSize and 4 images), removes about 3/4 of older cached images. /// Will auto-reload from files etc when need. /// public void CompactCache() { if (_a == null) return; //print.it(_cacheSize); if (CacheSize < MaxCacheSize || _a.Count < 4) return; CacheSize = 0; int n = _a.Count, max = MaxCacheSize / 4; while (CacheSize < max && n > 2) CacheSize += _a[--n].data.Length; _a.RemoveRange(0, n); } public int MaxCacheSize { get; set; } = 4 * 1024 * 1024; } [ThreadStatic] static _ThreadSharedData t_data; //all SciImages of a thread share single cache etc KScintilla _c; IntPtr _callbackPtr; const int c_indicImage = 7; /// /// Prepares this variable and the Scintilla control to display images. /// Note: will call SCI_ANNOTATIONSETVISIBLE(ANNOTATION_STANDARD) to draw images in annotation areas. /// Note: uses indicator 7. /// /// The control. internal SciImages(KScintilla c) { t_data ??= new(); _c = c; _sci_AnnotationDrawCallback = _AnnotationDrawCallback; _callbackPtr = Marshal.GetFunctionPointerForDelegate(_sci_AnnotationDrawCallback); _c.Call(SCI_SETANNOTATIONDRAWCALLBACK, 0, _callbackPtr); _c.aaaIndicatorDefine(c_indicImage, INDIC_HIDDEN); } _Image _GetImageFromText(RByte s) { //is it an image string? var imType = KImageUtil.ImageTypeFromString(out int prefixLength, s); if (imType == KImageUtil.ImageType.None) return null; if (prefixLength == 10) { s = s[prefixLength..]; prefixLength = 0; } //"imagefile:" var d = t_data; //is already loaded? long hash = Hash.Fnv1Long(s); var im = d.FindImage(hash, _c._dpi); //print.qm2.write(im != null, s.ToStringUTF8()); if (im != null) return im; string path = s[prefixLength..].ToStringUTF8(); //load long t1 = computer.tickCountWithoutSleep; byte[] b = KImageUtil.BmpFileDataFromString(path, imType, true, (_c._dpi, null)); t1 = computer.tickCountWithoutSleep - t1; if (t1 > 1000) print.warning($"Time to load image '{path}' is {t1} ms.", -1, prefix: "<>Note: "); //eg if network path unavailable, may wait ~7 s if (b == null) return null; if (!KImageUtil.GetBitmapFileInfo_(b, out var q)) return null; //create _Image im = new _Image() { data = b, nameHash = hash, width = q.width, height = Math.Min(q.height + IMAGE_MARGIN_TOP + IMAGE_MARGIN_BOTTOM, 2000) }; //add to cache //Compact cache to avoid memory problems when loaded many big images, eg showing all png in Program Files. //Will auto reload when need, it does not noticeably slow down. //Cache even very large images, because we draw each line separately, would need to load whole image for each line, which is VERY slow. d.CompactCache(); d.AddImage(im); return im; } /// /// Sets image annotations for one or more lines of text. /// Called at the end of SciTags._AddText if the added text contains image tags. /// internal void SetImagesForTextRange_(RByte text, List images, int prevLen) { bool allText = prevLen == 0; bool annotAdded = false; int iLine = -1, maxHeight = 0, totalWidth = 0; foreach (var v in images) { int line = _c.aaaLineFromPos(false, prevLen + v.start); if (line != iLine) { _AddLine(); (iLine, maxHeight, totalWidth) = (line, 0, 0); } for (int start = v.start, end = 0; start < v.end; start = end + 1) { //foreach image in image1|image2|image3 for (end = start; end < v.end && text[end] != '|';) end++; var s = text[start..end]; if (_GetImageFromText(s) is not _Image u) continue; if (maxHeight < u.height) maxHeight = u.height; if (totalWidth > 0) totalWidth += 30; totalWidth += u.width; _c.aaaIndicatorAdd(c_indicImage, false, (start + prevLen)..(end + prevLen)); } } _AddLine(); [SkipLocalsInit] void _AddLine() { if (maxHeight == 0) return; int annotLen = _c.Call(SCI_ANNOTATIONGETTEXT, iLine); //we'll need old annotation text later, and we'll get it into the same buffer after the new image info //calculate n annotation lines from image height int lineHeight = _c.aaaLineHeight(); if (lineHeight <= 0) return; int nAnnotLines = Math.Min((maxHeight + (lineHeight - 1)) / lineHeight, 255); //print.it(lineHeight, maxHeight, nAnnotLines); using FastBuffer buffer = new(annotLen + nAnnotLines + 20); var p = buffer.p; *p++ = 3; Api._ltoa(totalWidth << 8 | nAnnotLines, p, 16); while (*(++p) != 0) { } while (nAnnotLines-- > 1) *p++ = (byte)'\n'; *p = 0; //TODO2: code in this file can be simplified in several places. We don't use image+text annotations and images in editable text. Initially it was designed to support such images in code editor in editable mode. //An annotation possibly already exists. Possible cases: //1. No annotation. Need to add our image annotation. //2. A text-only annotation. Need to add our image annotation + that text. //3. Different image, no text. Need to replace it with our image annotation. //4. Different image + text. Need to replace it with our image annotation + that text. //5. This image, with or without text. Don't need to change. if (annotLen > 0) { //get existing annotation into the same buffer after our image info var a = p + 1; _c.Call(SCI_ANNOTATIONGETTEXT, iLine, a); a[annotLen] = 0; //print.it($"OLD: '{new string((sbyte*)a)}'"); //is it our image info? int imageLen = (int)(p - buffer.p); if (annotLen >= imageLen) { int j; for (j = 0; j < imageLen; j++) if (a[j] != buffer[j]) goto g1; if (annotLen == imageLen || a[imageLen] == '\n') return; //case 5 } g1: //contains image? if (a[0] == 3) { int j = _ParseAnnotText(a, annotLen, out var _); if (j < annotLen) { //case 4 Api.memmove(a, a + j, annotLen - j + 1); p[0] = (byte)'\n'; } //else case 3 } else { //case 2 p[0] = (byte)'\n'; } } //else case 1 //print.it($"NEW: '{new string((sbyte*)b0.p)}'"); //perf.first(); if (!annotAdded) { annotAdded = true; if (allText) _c.Call(SCI_ANNOTATIONSETVISIBLE, (int)AnnotationsVisible.ANNOTATION_HIDDEN); } _c.Call(SCI_ANNOTATIONSETTEXT, iLine, buffer.p); //perf.nw(); } if (annotAdded && allText) { _c.Call(SCI_ANNOTATIONSETVISIBLE, (int)AnnotationsVisible.ANNOTATION_STANDARD); } } /// /// Parses annotation text. /// If it starts with image info string ("\x3NNN\n\n..."), returns its length. Else returns 0. /// /// Annotation text. Can start with image info string or not. /// s length. /// The NNN part of image info, or 0. static int _ParseAnnotText(byte* s, int length, out int imageInfo) { imageInfo = 0; if (s == null || length < 4 || s[0] != '\x3') return 0; byte* s2; int k = Api.strtoi(s + 1, &s2, 16); int len = (int)(s2 - s); if (len < 4) return 0; int n = k & 0xff; len += (n - 1); if (n < 1 || length < len) return 0; if (length > len) len++; //\n between image info and visible annotation text imageInfo = k; return len; } /// /// Sets annotation text, preserving existing image info. /// /// /// New text without image info. [SkipLocalsInit] internal void AnnotationText_(int line, string s) { int n = _c.Call(SCI_ANNOTATIONGETTEXT, line); if (n > 0) { int lens = (s == null) ? 0 : s.Length; using FastBuffer buffer = new(n + 1 + lens * 3); var p = buffer.p; _c.Call(SCI_ANNOTATIONGETTEXT, line, p); p[n] = 0; int imageLen = _ParseAnnotText(p, n, out var _); if (imageLen > 0) { //info: now len<=n if (lens == 0) { if (imageLen == n) return; //no "\nPrevText" p[--imageLen] = 0; //remove "\nPrevText" } else { if (imageLen == n) p[imageLen++] = (byte)'\n'; //no "\nPrevText" //Convert2.Utf8FromString(s, p + imageLen, lens * 3); Encoding.UTF8.GetBytes(s, new Span(p + imageLen, lens * 3)); } _c.Call(SCI_ANNOTATIONSETTEXT, line, p); return; } } _c.aaaAnnotationText_(line, s); } /// /// Gets annotation text without image info. /// [SkipLocalsInit] internal string AnnotationText_(int line) { int n = _c.Call(SCI_ANNOTATIONGETTEXT, line); if (n > 0) { using FastBuffer buffer = new(n); var p = buffer.p; _c.Call(SCI_ANNOTATIONGETTEXT, line, p); p[n] = 0; int imageLen = _ParseAnnotText(p, n, out var _); //info: now len<=n if (imageLen < n) { if (imageLen != 0) { p += imageLen; n -= imageLen; } return Encoding.UTF8.GetString(p, n); } } return ""; } const int IMAGE_MARGIN_TOP = 2; //frame + 1 const int IMAGE_MARGIN_BOTTOM = 1; //just for frame. It is minimal margin, in most cases will be more. Sci_AnnotationDrawCallback _sci_AnnotationDrawCallback; unsafe int _AnnotationDrawCallback(void* cbParam, ref Sci_AnnotationDrawCallbackData c) { //Function info: //Called for all annotations, not just for images. //Returns image width. Returns 0 if there is no image or when called for annotation text line below image. //Called for each line of annotation, not once for whole image. Draws each image slice separately. //Called 2 times for each line: step 0 - to get width; step 1 - to draw that image slice on that line. Step 0 skipped if AnnotationsVisible.ANNOTATION_STANDARD (we don't use other styles). //Get image info from annotation text. Return 0 if there is no image info, ie no image. //Image info is at the start. Format "\x3XXX", where XXX is a hex number that contains image width and number of lines. byte* s = c.text; if (c.textLen < 4 || s[0] != '\x3') return 0; int k = Api.strtoi(++s, null, 16); if (k < 256) return 0; int nLines = k & 0xff, width = k >> 8; if (c.step == 0) return width + 1; //just get width if (c.annotLine >= nLines) return 0; //an annotation text line below the image lines //find image strings and draw the images bool hasImages = false; var hdc = c.hdc; IntPtr pen = default, oldPen = default; try { //Handle exceptions because SetDIBitsToDevice may read more than need, like CreateDIBitmap, although I never noticed this. int from = _c.aaaLineStart(false, c.line), to = _c.aaaLineEnd(false, c.line); RECT r = c.rect; int x = r.left + 1; for (int end = from; ;) { //for each `IMAGE` in this line, in text like `A b c` int start = _c.Call(SCI_INDICATOREND, c_indicImage, end); if (start <= 0 || start >= to) break; end = _c.Call(SCI_INDICATOREND, c_indicImage, start); if (end <= start) break; var u = _GetImageFromText(_c.aaaRangeSpan(start, end)); //find cached image or load and addd to the cache if (u is null) break; hasImages = true; //draw image (single slice, for this visual line) if (!KImageUtil.GetBitmapFileInfo_(u.data, out var q)) { Debug.Assert(false); continue; } int isFirstLine = (c.annotLine == 0) ? 1 : 0, hLine = r.bottom - r.top; int currentTop = c.annotLine * hLine, currentBottom = currentTop + hLine, imageBottom = q.height + IMAGE_MARGIN_TOP; int y = r.top + isFirstLine * IMAGE_MARGIN_TOP, yy = Math.Min(currentBottom, imageBottom) - currentTop; if (imageBottom > currentTop && q.width > 0 && q.height > 0) { fixed (byte* bp = u.data) { KImageUtil.BITMAPFILEHEADER* f = (KImageUtil.BITMAPFILEHEADER*)bp; byte* pBits = bp + f->bfOffBits; int bytesInLine = Math2.AlignUp(q.width * q.bitCount, 32) / 8; int sizF = u.data.Length - f->bfOffBits, siz = bytesInLine * q.height; if (q.isCompressed) { //this is slow with big images. It seems processes current line + all remaining lines. Such bitmaps are rare. int yOffs = -c.annotLine * hLine; if (isFirstLine == 0) yOffs += IMAGE_MARGIN_TOP; var ok = Api.SetDIBitsToDevice(hdc, x, r.top + isFirstLine * IMAGE_MARGIN_TOP, q.width, q.height, 0, yOffs, 0, q.height, pBits, q.biHeader); Debug.Assert(ok > 0); } else if (siz <= sizF) { //this is fast, but cannot use with compressed bitmaps int hei = yy - y, bmY = q.height - (currentTop - ((isFirstLine ^ 1) * IMAGE_MARGIN_TOP) + hei); var ok = Api.SetDIBitsToDevice(hdc, x, r.top + isFirstLine * IMAGE_MARGIN_TOP, q.width, hei, 0, 0, 0, hei, pBits + bmY * bytesInLine, q.biHeader); Debug.Assert(ok > 0); } else Debug.Assert(false); //could use this instead, but very slow with big images. It seems always processes whole bitmap, not just current line. //int hei=yy-y, bmY=q.height-(currentTop-((isFirstLine ^ 1)*IMAGE_MARGIN_TOP)+hei); //StretchDIBits(hdc, // x, y, q.width, hei, // 0, bmY, q.width, hei, // pBits, h, 0, SRCCOPY); } } //draw frame if (pen == default) oldPen = Api.SelectObject(hdc, pen = Api.CreatePen(0, 1, 0x60C060)); //quite fast. Caching in a static or ThreadStatic var is difficult. int xx = x + q.width; if (isFirstLine != 0) y--; if (yy > y) { Api.MoveToEx(hdc, x - 1, y, out _); Api.LineTo(hdc, x - 1, yy); //left | Api.MoveToEx(hdc, xx, y, out _); Api.LineTo(hdc, xx, yy); //right | if (isFirstLine != 0) { Api.MoveToEx(hdc, x, y, out _); Api.LineTo(hdc, xx, y); } //top _ } if (yy >= y && yy < hLine) { Api.MoveToEx(hdc, x - 1, yy, out _); Api.LineTo(hdc, xx + 1, yy); } //bottom _ x += u.width + 30; } } catch (Exception ex) { Debug_.Print(ex); } finally { if (pen != default) Api.DeleteObject(Api.SelectObject(hdc, oldPen)); } //perf.nw(); //If there are no image strings (text edited), delete the annotation or just its part containing image info and '\n's. if (!hasImages && c.annotLine == 0) { int line = c.line; var annot = AnnotationText_(line); //_c.aaaAnnotationText_(line, annot); //dangerous _c.Dispatcher.InvokeAsync(() => { _c.aaaAnnotationText_(line, annot); }); return 1; } return width + 1; //speed: fast. The fastest way. Don't need bitmap handle, memory DC, etc. //tested: don't know what ColorUse param of SetDIBitsToDevice does, but DIB_RGB_COLORS works for any h_biBitCount. //speed if drawing frame: multiple LineTo is faster than single PolyPolyline. //tested: GDI+ much slower, particularly DrawImage(). //tested: in QM2 was used LZO compression, now ZIP (DeflateStream). ZIP compresses better, but not so much. LZO is faster, but ZIP is fast enough. GIF and JPG in most cases compress less than ZIP and sometimes less than LZO. //tested: saving in 8-bit format in most cases does not make much smaller when compressed. For screenshots we reduce colors to 4-bit. } } ================================================ FILE: Au.Controls/KScintilla/SciTags.cs ================================================ /* Most tags are like in QM2. NEW TAGS: - bold italic. - monospace font. - font size (1-127). - collapsed lines. - select file in File Explorer. <\a>text - alternative for <_>text. - no newline. NEW PARAMETERS: - .NET color name for text color. Also color can be #RRGGBB. - .NET color name for background color. Also color can be #RRGGBB. - .NET color name for background color, whole line. Also color can be #RRGGBB. RENAMED TAGS: """; } #endif } /// /// Used by the Read panel. Retrieves local LA documentation files. /// Probably it's the best way to display local HTML files. Other ways have problems. /// class DocsHttpServer : HttpServerSession { static bool s_running; static int s_port; public static void StartOrSwitch() { if (App.Settings.doc_web || s_running) { _Switch(); } else { s_running = true; run.thread(() => { try { Listen(0, "127.0.0.1", listener => { s_port = ((System.Net.IPEndPoint)listener.LocalEndpoint).Port; _Switch(); }); } catch (Exception ex) { print.warning(ex); } App.Settings.doc_web = true; _Switch(); s_running = false; }, sta: false); } } public static string LocalBaseUri { get; private set; } static void _Switch() { if (App.Settings.doc_web) { LocalBaseUri = null; HelpUtil.AuHelpEvent_ -= _AuHelpEvent; } else { LocalBaseUri = $"http://127.0.0.1:{s_port}/"; HelpUtil.AuHelpEvent_ += _AuHelpEvent; } static void _AuHelpEvent(HelpUtil.AuHelpEventArgs_ e) { e.Cancel = true; Panels.Read.OpenDocUrl(e.Url); } } protected override void MessageReceived(HSMessage m, HSResponse r) { if (m.Method != "GET") { r.Status = System.Net.HttpStatusCode.MethodNotAllowed; return; } var path = m.TargetPath; if (path.Ends('/')) path += "index.html"; path = path[1..]; //print.it(path); #if DEBUG if (path == "cookbook/preview.html") { r.SetContentText(PanelRead.GetPreviewHtmlTemplate_(), "text/html; charset=utf-8"); return; } //this can be used when editing/testing a CSS file //if (path == "styles/docfx.vendor.min.css") { // r.SetContentText(filesystem.loadText(@"C:\Temp\Au\DocFX\site\styles\docfx.vendor.min.css"), "text/css"); // r.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate"; // return; //} #endif if (path == "favicon.ico" || path.Ends("/com.chrome.devtools.json")) { r.Status = System.Net.HttpStatusCode.NotFound; return; } lock (typeof(DocsHttpServer)) { //faster than the SQLite's busy timeout implementation using var db = new sqlite(folders.ThisAppBS + "doc-html.db", SLFlags.SQLITE_OPEN_READONLY); //fast, don't keep open if (db.Get(out byte[] content, "SELECT text FROM doc WHERE name=?", path)) { r.Content = content; var ext = pathname.getExtension(path).Lower(); var ct = ext switch { ".html" => "text/html; charset=utf-8", ".css" => "text/css", ".js" => "text/javascript", ".png" => "image/png", _ => null }; if (ct != null) r.Headers["Content-Type"] = ct; else Debug_.PrintIf(!(ext is ".eot"), path); if (ext != ".html") r.Headers["Cache-Control"] = "max-age=86400"; //1 day or until process exit } else { Debug_.PrintIf( !(path is "styles/docfx.vendor.min.css.map" /*size ~500kb, requested when showing devtools, works well without*/), "NOT FOUND: " + path); r.Status = System.Net.HttpStatusCode.NotFound; } } } } ================================================ FILE: Au.Editor/Panels/PanelTasks.cs ================================================ using Au.Controls; using System.Windows.Controls; using System.Windows.Input; namespace LA; class PanelTasks { KTreeView _tv; bool _updatedOnce; public PanelTasks() { //P.UiaSetName("Tasks panel"); //no UIA element for Panel _tv = new KTreeView { Name = "Tasks_list" }; P.Children.Add(_tv); Panels.PanelManager["Tasks"].DontActivateFloating = e => true; } public DockPanel P { get; } = new(); public void UpdateList() { _tv.SetItems(App.Tasks.Items, _updatedOnce); if (!_updatedOnce) { _updatedOnce = true; FilesModel.NeedRedraw += v => { _tv.Redraw(v.remeasure); }; _tv.ItemClick += _tv_ItemClick; } } private void _tv_ItemClick(TVItemEventArgs e) { if (e.Mod != 0 || e.ClickCount != 1) return; var t = e.Item as RunningTask; var f = t.f; switch (e.Button) { case MouseButton.Left: App.Model.SetCurrentFile(f); break; case MouseButton.Right: _tv.Select(t); var name = f.DisplayName; var m = new popupMenu { RawText = true }; m["End task '" + name + "'"] = _ => App.Tasks.EndTask(t); m["End all '" + name + "'"] = _ => App.Tasks.EndTasksOf(f); m.Separator(); m["Attach debugger"] = _ => Panels.Debug.Attach(t.processId); m.Separator(); m["Close\tM-click", disable: f.OpenDoc == null] = _ => App.Model.CloseFile(f, selectOther: true); //m.Separator(); //m["Recent tasks and triggers..."] = _ => RecentTT.Show(); //rejected. It is in menu Run. Or would also need to show context menu when rclicked in empty space. m.Show(); break; case MouseButton.Middle: App.Model.CloseFile(f, selectOther: true); break; } } } ================================================ FILE: Au.Editor/Panels/Panels.cs ================================================ using Au.Controls; using System.Windows.Controls; using System.Windows; using System.Windows.Input; using System.Xml.Linq; using System.Xml.XPath; using System.Windows.Media; namespace LA; static class Panels { public static KPanels PanelManager; //panels public static PanelEdit Editor; public static PanelFiles Files; public static PanelOutline Outline; public static PanelHelp Help; public static PanelOpen Open; public static PanelTasks Tasks; public static PanelOutput Output; public static PanelFind Find; public static PanelFound Found; public static PanelMouse Mouse; public static PanelRead Read; public static PanelBookmarks Bookmarks; public static PanelBreakpoints Breakpoints; public static PanelDebug Debug; //menu and toolbars public static Menu Menu; public static ToolBar TFile, TEdit, TRun, TTools, TCustom1, TCustom2; public static void LoadAndCreateToolbars() { var pm = PanelManager = new KPanels(); var customLayoutPath = AppSettings.DirBS + "Layout.xml"; if (filesystem.exists(customLayoutPath).File) { try { var x = XmlUtil.LoadElem(customLayoutPath); if (x.XPathSelectElement("//panel[@name='Outline']") == null) { //v0.4 added several new panels etc, and users would not know the best place for them, or even how to move filesystem.delete(customLayoutPath, FDFlags.RecycleBin); print.it("Info: The window layout has been reset, because several new panels have been added in this app version.\r\n\tIf you want to undo it: 1. Exit the program. 2. Restore file Layout.xml from the Recycle Bin (replace the existing file). 3. Run the program. 4. Move panels from the bottom of the window to a better place."); } else if (x.XPathSelectElement("//panel[@name='Help']") == null) { //in v1.15 renamed some panels and removed the Help toolbar x.XPathSelectElement("//toolbar[@name='Help']")?.Remove(); x.XPathSelectElement("//panel[@name='Cookbook']")?.SetAttributeValue("name", "Help"); x.XPathSelectElement("//panel[@name='Recipe']")?.SetAttributeValue("name", "Read"); //also renamed some attributes foreach (var v in x.Descendants()) { if (v.Attribute("captionAt") is { } k) { v.SetAttributeValue("headerAt", k.Value); k.Remove(); } } x.SaveElem(customLayoutPath); //also remove the Help toolbar from toolbar customizations var customCommandsPath = AppSettings.DirBS + "Commands.xml"; if (filesystem.exists(customCommandsPath).File) { var xc = XmlUtil.LoadElem(customCommandsPath); if (xc.XPathSelectElement("//Help") is { } x1) { x1.Remove(); xc.Save(customCommandsPath); } } } } catch (Exception e1) { Debug_.Print(e1); } } pm.BorderBrush = SystemColors.ActiveBorderBrush; //pm.Load(folders.ThisAppBS + @"Default\Layout.xml", null); pm.Load(folders.ThisAppBS + @"Default\Layout.xml", customLayoutPath); int saveCounter = 0; App.Timer1sWhenVisible += () => { if (++saveCounter >= 60) { saveCounter = 0; pm.Save(); } }; pm["Menu"].Content = Menu = new Menu(); TFile = _CreateToolbar("File"); TEdit = _CreateToolbar("Edit"); TRun = _CreateToolbar("Run"); TTools = _CreateToolbar("Tools"); TCustom1 = _CreateToolbar("Custom1"); TCustom2 = _CreateToolbar("Custom2"); } static ToolBar _CreateToolbar(string name, Func dockPanel = null) { var c = new ToolBar { Name = name/*, Background = SystemColors.ControlBrush*/ }; c.UiaSetName(name); c.HideGripAndOverflow(false); var tt = new ToolBarTray { IsLocked = true/*, Background = SystemColors.ControlBrush*/ }; //because ToolBar looks bad if parent is not ToolBarTray tt.ToolBars.Add(c); FrameworkElement content = tt; if (dockPanel != null) { var p = new DockPanel { Background = tt.Background }; p.Children.Add(tt); DockPanel.SetDock(tt, dockPanel(p)); content = p; } PanelManager[name].Content = content; c.ContextMenuOpening += DCustomize.ToolbarContextMenuOpening; c.PreviewMouseRightButtonDown += (o, e) => { //prevent closing the overflow panel on right mouse button down if ((e.OriginalSource as UIElement).VisualAncestors(true).Any(o => o is System.Windows.Controls.Primitives.ToolBarOverflowPanel)) e.Handled = true; }; return c; } public static void CreatePanels() { var pm = PanelManager; KPanels.ILeaf _AddDontFocus(string panel, FrameworkElement content) { var p = pm[panel]; p.Content = content; p.DontFocusTab = () => { var doc = Panels.Editor.ActiveDoc; if (doc != null) doc.Focus(); else Keyboard.ClearFocus(); }; return p; } pm["documents"].Content = (Editor = new()).P; pm["Files"].Content = (Files = new()).P; _AddDontFocus("Outline", (Outline = new()).P); pm["Help"].Content = (Help = new()).P; _AddDontFocus("Debug", (Debug = new()).P); _AddDontFocus("Open", (Open = new()).P); _AddDontFocus("Tasks", (Tasks = new()).P); pm["Find"].Content = (Find = new()).P; _AddDontFocus("Bookmarks", (Bookmarks = new()).P); _AddDontFocus("Breakpoints", (Breakpoints = new()).P); _AddDontFocus("Output", (Output = new()).P); _AddDontFocus("Mouse", (Mouse = new()).P); _AddDontFocus("Found", (Found = new()).P); var pRead = _AddDontFocus("Read", (Read = new()).P); pRead.Visible = false; } } ================================================ FILE: Au.Editor/Properties/launchSettings.json ================================================ { "profiles": { "Au.Editor": { "commandName": "Project", "nativeDebugging": false } } } ================================================ FILE: Au.Editor/Test.cs ================================================ #if DEBUG || IDE_LA extern alias CAW; using Microsoft.CodeAnalysis; using CAW::Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Shared.Extensions; using CAW::Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.Shared.Utilities; using CAW::Microsoft.CodeAnalysis.Shared.Utilities; using CAW::Microsoft.CodeAnalysis.FindSymbols; using Au.Triggers; using Au.Controls; using System.Windows.Controls; //using System.Windows.Forms; using System.Text.RegularExpressions; using System.Diagnostics.CodeAnalysis; using static Au.Controls.Sci; namespace LA; static class Test { /// /// /// public static void FromMenubar() { //print.clear(); print.it(Panels.Editor.ActiveDoc.aaaCurrentPos16); //var query = Panels.Editor.ActiveDoc.aaaText.Lines()[0]; //var query = Panels.Editor.ActiveDoc.aaaText; //Task.Run(() => { // try { // McpTools _tools = new(); // var s = _tools.find_la_docs(query, ""); // print.scrollToTop(); // } // catch (Exception ex) { print.it(ex); } //}); //print.it(s); //timer2.every(500, _=> { GC.Collect(); }); //Cpp.Cpp_Test(); #if !IDE_LA #endif } public static void MonitorGC() { //if(!s_debug2) { // s_debug2 = true; // new TestGC(); // //timer.every(50, _ => { // // if(!s_debug) { // // s_debug = true; // // timer.after(100, _ => new TestGC()); // // } // //}); //} } //static bool s_debug2; class TestGC { ~TestGC() { if (Environment.HasShutdownStarted) return; if (AppDomain.CurrentDomain.IsFinalizingForUnload()) return; print.it("GC", GC.CollectionCount(0), GC.CollectionCount(1), GC.CollectionCount(2)); //timer.after(1, _ => new TestGC()); //var f = App.Wmain; if(!f.IsHandleCreated) return; //f.BeginInvoke(new Action(() => new TestGC())); new TestGC(); } } } #endif ================================================ FILE: Au.Editor/Tools/CapturingWithHotkey.cs ================================================ using Au.Controls; using System.Windows.Interop; using System.Windows; using System.Windows.Controls; namespace ToolLand; static partial class TUtil { /// /// Common code for tools that capture UI objects with F3. /// public class CapturingWithHotkey { public record struct Hothey(string hotkey, Action a); readonly bool _isElm; readonly wnd _wDialog; readonly KCheckBox _captureCheckbox; readonly Func _getRect; readonly Hothey _hkCapture, _hkInsert, _hkSmaller; HwndSource _hs; timer _timer; osdRect _osr; osdText _ost; //TODO3: draw rect and text in same OsdWindow bool _capturing; const string c_propName = "Au.Capture"; readonly static int s_stopMessage = Api.RegisterWindowMessage(c_propName); const int c_hotkeyCapture = 1623031890, c_hotkeyInsert = 1623031891, c_hotkeySmaller = 1623031892; /// Checkbox that turns on/off capturing. /// Called to get rectangle of object from mouse. Receives mouse position. Can return default to hide the rectangle. public CapturingWithHotkey(KCheckBox captureCheckbox, Func getRect, Hothey capture, Hothey insert = default, Hothey smaller = default) { _wDialog = captureCheckbox.Hwnd(); _captureCheckbox = captureCheckbox; _getRect = getRect; _hkCapture = capture; _hkInsert = insert; _hkSmaller = smaller; _isElm = smaller.a != null; } /// /// Starts or stops capturing. /// Does nothing if already in that state. /// public bool Capturing { get => _capturing; set { if (value == _capturing) return; if (value) _StartCapturing(); else _StopCapturing(); } } void _StopCapturing() { _capturing = false; _timer.Stop(); _HideRect(); _hs.RemoveHook(_WndProc); Api.UnregisterHotKey(_wDialog, c_hotkeyCapture); if (_hkInsert.hotkey != null) Api.UnregisterHotKey(_wDialog, c_hotkeyInsert); if (_hkSmaller.hotkey != null) Api.UnregisterHotKey(_wDialog, c_hotkeySmaller); _wDialog.Prop.Remove(c_propName); } void _StartCapturing() { //let other tools stop capturing. Any process. wnd.find(null, "HwndWrapper[*", flags: WFlags.HiddenToo | WFlags.CloakedToo, also: o => { if (o != _wDialog && o.Prop[c_propName] == 1) o.SendTimeout(3000, out _, s_stopMessage); return false; }); _wDialog.Prop.Set(c_propName, 1); //register hotkeys bool _RegisterHotkey(int id, string hotkey) { string es = null; try { var (mod, key) = RegisteredHotkey.Normalize_(hotkey); if (Api.RegisterHotKey(_wDialog, id, mod, key)) return true; es = "Failed to register."; } catch (Exception e1) { es = e1.Message; } dialog.showError("Hotkey " + hotkey, es + "\nClick the hotkey link to set another hotkey.", owner: _wDialog); return false; } if (!_RegisterHotkey(c_hotkeyCapture, _hkCapture.hotkey)) return; if (_hkInsert.hotkey != null) _RegisterHotkey(c_hotkeyInsert, _hkInsert.hotkey); if (_hkSmaller.hotkey != null) _RegisterHotkey(c_hotkeySmaller, _hkSmaller.hotkey); //hook wndproc if (_hs == null) { _hs = PresentationSource.FromDependencyObject(_captureCheckbox) as HwndSource; _hs.Disposed += (_, _) => { Capturing = false; _osr?.Dispose(); _ost?.Dispose(); }; } _hs.AddHook(_WndProc); //set timer to show rectangle of UI element from mouse if (_timer == null) { _osr = CreateOsdRect(2); _timer = new timer(_ => { if (!_capturing) return; POINT p = mouse.xy; if (_isElm) { _elmTimer.Timer(p); } else { _ShowRect(p, wnd.fromXY(p, WXYFlags.NeedWindow)); } }); } if (_isElm) _elmTimer = new(this); _timer.Every(_isElm ? 50 : 200); _capturing = true; } _ElmTimer _elmTimer; class _ElmTimer { CapturingWithHotkey _capt; POINT _pMM; bool _mouseMoved; long _timeMM, _timeUpdated; wnd _w; public _ElmTimer(CapturingWithHotkey capt) { _capt = capt; _pMM.x = int.MinValue; } public void Timer(POINT p) { if (mouse.isPressed()) { _capt._HideRect(); if (p == _pMM) return; } //get rect when mouse stops. With a delay if stops in a new window. if (p != _pMM) { if (_capt._osr.Visible && !_capt._osr.Rect.Contains(p)) _capt._HideRect(); _pMM = p; _mouseMoved = true; _timeMM = Environment.TickCount64; } else { wnd w = wnd.fromXY(p, WXYFlags.NeedWindow); bool sameWindow = !_w.Is0 && (w == _w || w.ThreadId == _w.ThreadId); bool update = false; bool idle = sameWindow && !_mouseMoved; if (idle) { //repeat _ShowRect anyway, because may scroll, close window, etc long timeNow = Environment.TickCount64; long liTime = Api.GetLastInputTime(); if (timeNow - liTime > 700) { long timeNotUpdated = timeNow - _timeUpdated; update = timeNotUpdated > 1000 || (liTime > _timeUpdated && timeNotUpdated > 300); } } else { update = sameWindow || Environment.TickCount64 - _timeMM >= 230; } if (update) { //print.it("update rect"); long t1 = Environment.TickCount64; _capt._ShowRect(p, _w = w); long t2 = Environment.TickCount64; _timeUpdated = t2 + (t2 - t1) * 2; } _mouseMoved = false; } } } void _ShowRect(POINT p, wnd w) { bool ok = false; RECT r = default; string text = null; if (!(w.Is0 || w == _wDialog || (w.IsOfThisThread && w.ZorderIsAbove(_wDialog)))) { var k = new GetRectArgs(p, w); if (ok = _getRect(k)) (r, text) = (k.resultRect, k.resultText); //F3 does not work if this process has lower UAC IL than the foreground process. // Normally editor is admin, but if portable etc... // Shift+F3 too. But Ctrl+F3 works. //if (w!=wPrev && w.IsActive) { // w = wPrev; // if(w.UacAccessDenied)print.it("F3 "); //} } if (ok && !t_hideCapturingRect) { r.Inflate(1, 1); //1 pixel inside, 1 outside _LimitInsaneRect(ref r); _osr.Rect = r; _osr.Show(); if (!text.NE()) { _ost ??= new() { Font = new(8), Shadow = false, ShowMode = OsdMode.ThisThread, SecondsTimeout = -1 }; _ost.Text = text; var ro = _ost.Measure(); var rs = screen.of(r).Rect; int x = r.left, y = r.top + 8; if (r.top - rs.top >= ro.Height) y = r.top - ro.Height; else if (r.Height < 200) y = r.bottom; else x += 8; _ost.XY = new(x, y, false); _ost.Show(); } else _ost?.Visible = false; } else { _HideRect(); } } void _HideRect() { _osr.Hide(); _ost?.Hide(); } public record class GetRectArgs(POINT p, wnd wTL) { public RECT resultRect; public string resultText; } nint _WndProc(nint hwnd, int msg, nint wParam, nint lParam, ref bool handled) { if (msg == s_stopMessage) { handled = true; _captureCheckbox.IsChecked = false; } else if (msg == Api.WM_HOTKEY && (wParam is c_hotkeyCapture or c_hotkeyInsert or c_hotkeySmaller)) { handled = true; if (wParam == c_hotkeyInsert) _hkInsert.a(); else if (wParam == c_hotkeySmaller) _hkSmaller.a(); else _hkCapture.a(); } return default; } /// /// Adds link +hotkey that shows dialog "Hotkeys" and updates LA.App.Settings.delm.hk_x. /// public static void RegisterLink_DialogHotkey(KSciInfoBox sci) { if (sci.AaTags.HasLinkTag("+hotkey")) return; sci.AaTags.AddLinkTag("+hotkey", _ => { var b = new wpfBuilder("Hotkey"); b.R.xAddGroupSeparator("In wnd and elm tools"); b.R.Add("Capture", out TextBox capture, LA.App.Settings.delm.hk_capture).xValidateHotkey(errorIfEmpty: true).Focus(); b.R.xAddGroupSeparator("In elm tool"); b.R.Add("Insert code", out TextBox insert, LA.App.Settings.delm.hk_insert).xValidateHotkey(); b.R.Add("Capturing method", out TextBox smaller, LA.App.Settings.delm.hk_smaller).xValidateHotkey(); b.R.AddSeparator(false); b.R.xAddInfoBlockT("After changing hotkeys please restart the tool window."); if (!uacInfo.isAdmin) b.R.xAddInfoBlockT("Hotkeys don't work when the active window is admin,\nbecause this process isn't admin."); b.R.AddOkCancel(); b.End(); if (b.ShowDialog(Window.GetWindow(sci))) { LA.App.Settings.delm.hk_capture = capture.Text; LA.App.Settings.delm.hk_insert = insert.TextOrNull(); LA.App.Settings.delm.hk_smaller = smaller.TextOrNull(); } }); } public UsingEndAction TempHideRect() { bool v1, v2; if (v1 = _osr.Visible) _osr.Hwnd.ShowL(false); if (v2 = _ost?.Visible == true) _ost.Hwnd.ShowL(false); return new(() => { if (v1) _osr.Hwnd.ShowL(true); if (v2) _ost.Hwnd.ShowL(true); }); } } } ================================================ FILE: Au.Editor/Tools/ColorQuantizer.cs ================================================ using System.Drawing; using System.Drawing.Imaging; namespace ToolLand; /// /// Reduces image color depth with better quality than GDI/GDI+. It makes image smaller. /// Uses color quantizer algorithm of Xiaolin Wu. /// Same instance can be reused to quantize multiple images and avoid 0.7 MB of garbage for each; the static functions use this. /// unsafe class ColorQuantizer { /// /// Takes screenshot of specified rectangle in screen, quantizes colors to make smaller, compresses, Base64 encodes and returns comment string like " /*image:...*/". /// If fails, prints warning and returns null. /// /// Rectangle in screen. Must be not DPI-scaled. This function inflates it if need for DPI of that screen. public static string MakeScreenshotComment(RECT r) { int dpi = screen.of(r).Dpi; if (dpi != 96) { r.Inflate((Dpi.Scale(r.Width, dpi) - r.Width) / 2, (Dpi.Scale(r.Height, dpi) - r.Height) / 2); } try { var b = CaptureScreen.Image(r); var a = Quantize(b, 16, dpi); var z = Convert2.BrotliCompress(a); return "/*image:" //+ "\r\n" //rejected + "WkJN" + Convert.ToBase64String(z) + "*/"; } catch (Exception e1) { print.warning("MakeScreenshotComment() failed. " + e1.ToStringWithoutStack()); return null; } } [ThreadStatic] static WeakReference t_wr; /// /// Quantizes bitmap. /// /// Bitmap with pixel bit count 32 or 24. If 32, alpha is ignored. /// Max count of colors desired. Must be 2 to 256 inclusive. For example a 4-bit bitmap can have max 16 colors; 8-bit - max 256 colors. /// Image resolution as DPI. If more than 96, this function writes this info to the bitmap header. /// .bmp file data (BITMAPFILEHEADER, BITMAPINFOHEADER, color table, pixel bits) of quantized bitmap. /// public static byte[] Quantize(Bitmap b, int nColors, int dpi = 0) { t_wr ??= new(null); if (!t_wr.TryGetTarget(out var q)) t_wr.SetTarget(q = new()); //avoid 0.7 MB of garbage each time using var d = b.Data(ImageLockMode.ReadOnly); var p = (byte*)d.Scan0; bool topDown = d.Stride >= 0; if (!topDown) p -= -d.Stride * (d.Height - 1); return q.Quantize(d.Width, d.Height, Image.GetPixelFormatSize(d.PixelFormat), p, ref nColors, topDown, dpi); } /// /// Quantizes bitmap. /// /// Bitmap with pixel bit count 32 or 24. If 32, alpha is ignored. /// Max count of colors desired. Must be 2 to 256 inclusive. For example a 4-bit bitmap can have max 16 colors; 8-bit - max 256 colors. /// Image resolution as DPI. If more than 96, this function writes this info to the bitmap header. /// Quantized bitmap. /// public static Bitmap QuantizeB(Bitmap b, int nColors, int dpi = 0) => new(new MemoryStream(Quantize(b, nColors, dpi))); // DIB data int _width, _height, _lineSize, _colorBytes; byte* _bits; struct Box { public int r0, r1, g0, g1, b0, b1, vol; } const int FI_RGBA_RED = 2, FI_RGBA_GREEN = 1, FI_RGBA_BLUE = 0; const int FIQ_SIZE_3D = 33 * 33 * 33; float[] _gm2; int[] _wt, _mr, _mg, _mb; ushort[] Qadd; byte[] _tag; /// /// Quantizes bitmap. /// /// /// /// Pixel bit count of the input bitmap. Must be 32 or 24. If 32, alpha is ignored. /// Pixels. /// Input - max count of colors desired. Must be 2 to 256 inclusive. For example a 4-bit bitmap can have max 16 colors; 8-bit - max 256 colors. Output - actual count of colors in color table of the returned bitmap. /// Top-down bitmap. For example GDI+ (System.Drawing) bitmaps usually are top-down. /// Image resolution as DPI. If more than 96, this function writes this info to the bitmap header. /// .bmp file data (BITMAPFILEHEADER, BITMAPINFOHEADER, color table, pixel bits) of quantized bitmap. /// public byte[] Quantize(int width, int height, int bitCount, byte* bits, ref int nColors, bool topDown, int dpi = 0) { if (!(bits != null && bitCount is (32 or 24) && width > 0 && height > 0 && nColors >= 2 && nColors <= 256)) throw new ArgumentException(); _width = width; _height = height; _lineSize = _LineSize(bitCount); _colorBytes = bitCount / 8; _bits = bits; //Debug_.MemorySetAnchor_(); static void _Array3D(ref T[] a) { if (a == null) a = new T[FIQ_SIZE_3D]; else Array.Clear(a, 0, a.Length); } _Array3D(ref _gm2); _Array3D(ref _wt); _Array3D(ref _mr); _Array3D(ref _mg); _Array3D(ref _mb); _Array3D(ref _tag); Qadd = new ushort[_width * _height]; var cube = stackalloc Box[256]; int next; int i, weight; int k; var vv = stackalloc float[256]; float temp; // Compute 3D histogram Hist3D(_wt, _mr, _mg, _mb, _gm2); // Compute moments M3D(_wt, _mr, _mg, _mb, _gm2); cube[0].r0 = cube[0].g0 = cube[0].b0 = 0; cube[0].r1 = cube[0].g1 = cube[0].b1 = 32; next = 0; for (i = 1; i < nColors; i++) { if (Cut(ref cube[next], ref cube[i])) { // volume test ensures we won't try to cut one-cell box vv[next] = (cube[next].vol > 1) ? Var(cube[next]) : 0; vv[i] = (cube[i].vol > 1) ? Var(cube[i]) : 0; } else { vv[next] = 0f; // don't try to split this box again i--; // didn't create box i } next = 0; temp = vv[0]; for (k = 1; k <= i; k++) { if (vv[k] > temp) { temp = vv[k]; next = k; } } if (temp <= 0.0) { nColors = i + 1; // Error: "Only got 'nColors' boxes" break; } } // Partition done // Allocate a new dib int bpp = nColors > 16 ? 8 : 4, lineSize = _LineSize(bpp), paletteSize = nColors * 4, bitsSize = _height * lineSize; int headersSize = sizeof(BITMAPFILEHEADER) + sizeof(Api.BITMAPINFOHEADER), bitsOffset = headersSize + paletteSize; var ret = new byte[bitsOffset + bitsSize]; fixed (byte* pret = ret) { // create an optimized palette var new_pal = (RGBQUAD*)(pret + headersSize); for (k = 0; k < nColors; k++) { Mark(cube[k], k, _tag); weight = Vol(cube[k], _wt); if (weight != 0) { new_pal[k].rgbRed = (byte)(((float)Vol(cube[k], _mr) / (float)weight) + 0.5f); new_pal[k].rgbGreen = (byte)(((float)Vol(cube[k], _mg) / (float)weight) + 0.5f); new_pal[k].rgbBlue = (byte)(((float)Vol(cube[k], _mb) / (float)weight) + 0.5f); } else { // Error: bogus box 'k' new_pal[k].rgbRed = new_pal[k].rgbGreen = new_pal[k].rgbBlue = 0; } } // create pixel bits var newBits = pret + bitsOffset; for (int y = 0; y < _height; y++) { byte* new_bits = newBits + (y * lineSize); int yy = topDown ? _height - y - 1 : y; //always save as bottom-up int lineStart = yy * _width; if (bpp == 8) { for (int x = 0; x < _width; x++) new_bits[x] = _tag[Qadd[lineStart + x]]; } else { int x, n = _width & ~1; //because using x+1 for (x = 0; x < n; x += 2) new_bits[x / 2] = (byte)((_tag[Qadd[lineStart + x]] << 4) | (_tag[Qadd[lineStart + x + 1]] & 0xF)); if (n != _width) new_bits[x / 2] = (byte)(_tag[Qadd[lineStart + x]] << 4); } } // output 'new_pal' as color look-up table contents, // 'new_bits' as the quantized image (array of table addresses). var f2 = (BITMAPFILEHEADER*)pret; f2->bfType = Math2.MakeWord('B', 'M'); f2->bfSize = ret.Length; f2->bfOffBits = bitsOffset; var h2 = (Api.BITMAPINFOHEADER*)(f2 + 1); h2->biSize = sizeof(Api.BITMAPINFOHEADER); h2->biPlanes = 1; h2->biBitCount = (ushort)bpp; h2->biClrUsed = nColors; h2->biWidth = _width; h2->biHeight = _height; if (dpi > 96) h2->biXPelsPerMeter = h2->biYPelsPerMeter = (dpi * 39.370079).ToInt(); } //Debug_.MemoryPrint_(); return ret; int _LineSize(int bitCount) => Math2.AlignUp(_width * bitCount, 32) / 8; } // 3D array indexation [MethodImpl(MethodImplOptions.AggressiveInlining)] static int FIQ_INDEX(int r, int g, int b) => (r << 10) + (r << 6) + r + (g << 5) + g + b; // Histogram is in elements 1..HISTSIZE along each axis, // element 0 is for base or marginal value // NB: these must start out 0! // Build 3-D color histogram of counts, r/g/b, c^2 void Hist3D(int[] vwt, int[] vmr, int[] vmg, int[] vmb, float[] m2) { int ind = 0; int inr, ing, inb; var table = stackalloc int[256]; int i, y, x; for (i = 0; i < 256; i++) table[i] = i * i; for (y = 0; y < _height; y++) { var bits = _bits + y * _lineSize; for (x = 0; x < _width; x++) { inr = (bits[FI_RGBA_RED] >> 3) + 1; ing = (bits[FI_RGBA_GREEN] >> 3) + 1; inb = (bits[FI_RGBA_BLUE] >> 3) + 1; ind = FIQ_INDEX(inr, ing, inb); Qadd[y * _width + x] = (ushort)ind; // [inr][ing][inb] vwt[ind]++; vmr[ind] += bits[FI_RGBA_RED]; vmg[ind] += bits[FI_RGBA_GREEN]; vmb[ind] += bits[FI_RGBA_BLUE]; m2[ind] += (float)(table[bits[FI_RGBA_RED]] + table[bits[FI_RGBA_GREEN]] + table[bits[FI_RGBA_BLUE]]); bits += _colorBytes; } } } //___________________________________________ // At conclusion of the histogram step, we can interpret // wt[r][g][b] = sum over voxel of P(c) // mr[r][g][b] = sum over voxel of r*P(c) , similarly for mg, mb // m2[r][g][b] = sum over voxel of c^2*P(c) // Actually each of these should be divided by 'ImageSize' to give the usual // interpretation of P() as ranging from 0 to 1, but we needn't do that here. // We now convert histogram into moments so that we can rapidly calculate // the sums of the above quantities over any desired box. // Compute cumulative moments static void M3D(int[] vwt, int[] vmr, int[] vmg, int[] vmb, float[] m2) { int ind1, ind2; byte i, r, g, b; int line, line_r, line_g, line_b; int* area = stackalloc int[33], area_r = stackalloc int[33], area_g = stackalloc int[33], area_b = stackalloc int[33]; float line2; var area2 = stackalloc float[33]; for (r = 1; r <= 32; r++) { for (i = 0; i <= 32; i++) { area2[i] = 0; area[i] = area_r[i] = area_g[i] = area_b[i] = 0; } for (g = 1; g <= 32; g++) { line2 = 0; line = line_r = line_g = line_b = 0; for (b = 1; b <= 32; b++) { ind1 = FIQ_INDEX(r, g, b); // [r][g][b] line += vwt[ind1]; line_r += vmr[ind1]; line_g += vmg[ind1]; line_b += vmb[ind1]; line2 += m2[ind1]; area[b] += line; area_r[b] += line_r; area_g[b] += line_g; area_b[b] += line_b; area2[b] += line2; ind2 = ind1 - 1089; // [r-1][g][b] vwt[ind1] = vwt[ind2] + area[b]; vmr[ind1] = vmr[ind2] + area_r[b]; vmg[ind1] = vmg[ind2] + area_g[b]; vmb[ind1] = vmb[ind2] + area_b[b]; m2[ind1] = m2[ind2] + area2[b]; } } } } //___________________________________________ // Compute sum over a box of any given statistic static int Vol(in Box cube, int[] mmt) { return mmt[FIQ_INDEX(cube.r1, cube.g1, cube.b1)] - mmt[FIQ_INDEX(cube.r1, cube.g1, cube.b0)] - mmt[FIQ_INDEX(cube.r1, cube.g0, cube.b1)] + mmt[FIQ_INDEX(cube.r1, cube.g0, cube.b0)] - mmt[FIQ_INDEX(cube.r0, cube.g1, cube.b1)] + mmt[FIQ_INDEX(cube.r0, cube.g1, cube.b0)] + mmt[FIQ_INDEX(cube.r0, cube.g0, cube.b1)] - mmt[FIQ_INDEX(cube.r0, cube.g0, cube.b0)]; } //___________________________________________ // The next two routines allow a slightly more efficient calculation // of Vol() for a proposed subbox of a given box. The sum of Top() // and Bottom() is the Vol() of a subbox split in the given direction // and with the specified new upper bound. // Compute part of Vol(cube, mmt) that doesn't depend on r1, g1, or b1 // (depending on dir) static int Bottom(in Box cube, byte dir, int[] mmt) { switch (dir) { case FI_RGBA_RED: return -mmt[FIQ_INDEX(cube.r0, cube.g1, cube.b1)] + mmt[FIQ_INDEX(cube.r0, cube.g1, cube.b0)] + mmt[FIQ_INDEX(cube.r0, cube.g0, cube.b1)] - mmt[FIQ_INDEX(cube.r0, cube.g0, cube.b0)]; case FI_RGBA_GREEN: return -mmt[FIQ_INDEX(cube.r1, cube.g0, cube.b1)] + mmt[FIQ_INDEX(cube.r1, cube.g0, cube.b0)] + mmt[FIQ_INDEX(cube.r0, cube.g0, cube.b1)] - mmt[FIQ_INDEX(cube.r0, cube.g0, cube.b0)]; case FI_RGBA_BLUE: return -mmt[FIQ_INDEX(cube.r1, cube.g1, cube.b0)] + mmt[FIQ_INDEX(cube.r1, cube.g0, cube.b0)] + mmt[FIQ_INDEX(cube.r0, cube.g1, cube.b0)] - mmt[FIQ_INDEX(cube.r0, cube.g0, cube.b0)]; } return 0; } //___________________________________________ // Compute remainder of Vol(cube, mmt), substituting pos for // r1, g1, or b1 (depending on dir) static int Top(in Box cube, byte dir, int pos, int[] mmt) { switch (dir) { case FI_RGBA_RED: return mmt[FIQ_INDEX(pos, cube.g1, cube.b1)] - mmt[FIQ_INDEX(pos, cube.g1, cube.b0)] - mmt[FIQ_INDEX(pos, cube.g0, cube.b1)] + mmt[FIQ_INDEX(pos, cube.g0, cube.b0)]; case FI_RGBA_GREEN: return mmt[FIQ_INDEX(cube.r1, pos, cube.b1)] - mmt[FIQ_INDEX(cube.r1, pos, cube.b0)] - mmt[FIQ_INDEX(cube.r0, pos, cube.b1)] + mmt[FIQ_INDEX(cube.r0, pos, cube.b0)]; case FI_RGBA_BLUE: return mmt[FIQ_INDEX(cube.r1, cube.g1, pos)] - mmt[FIQ_INDEX(cube.r1, cube.g0, pos)] - mmt[FIQ_INDEX(cube.r0, cube.g1, pos)] + mmt[FIQ_INDEX(cube.r0, cube.g0, pos)]; } return 0; } //___________________________________________ // Compute the weighted variance of a box // NB: as with the raw statistics, this is really the variance * ImageSize float Var(in Box cube) { float dr = (float)Vol(cube, _mr); float dg = (float)Vol(cube, _mg); float db = (float)Vol(cube, _mb); float xx = _gm2[FIQ_INDEX(cube.r1, cube.g1, cube.b1)] - _gm2[FIQ_INDEX(cube.r1, cube.g1, cube.b0)] - _gm2[FIQ_INDEX(cube.r1, cube.g0, cube.b1)] + _gm2[FIQ_INDEX(cube.r1, cube.g0, cube.b0)] - _gm2[FIQ_INDEX(cube.r0, cube.g1, cube.b1)] + _gm2[FIQ_INDEX(cube.r0, cube.g1, cube.b0)] + _gm2[FIQ_INDEX(cube.r0, cube.g0, cube.b1)] - _gm2[FIQ_INDEX(cube.r0, cube.g0, cube.b0)]; return (xx - (dr * dr + dg * dg + db * db) / (float)Vol(cube, _wt)); } //___________________________________________ // We want to minimize the sum of the variances of two subboxes. // The sum(c^2) terms can be ignored since their sum over both subboxes // is the same (the sum for the whole box) no matter where we split. // The remaining terms have a minus sign in the variance formula, // so we drop the minus sign and MAXIMIZE the sum of the two terms. float Maximize(in Box cube, byte dir, int first, int last, out int cut, int whole_r, int whole_g, int whole_b, int whole_w) { int half_r, half_g, half_b, half_w; int i; float temp; int base_r = Bottom(cube, dir, _mr); int base_g = Bottom(cube, dir, _mg); int base_b = Bottom(cube, dir, _mb); int base_w = Bottom(cube, dir, _wt); float max = 0f; cut = -1; for (i = first; i < last; i++) { half_r = base_r + Top(cube, dir, i, _mr); half_g = base_g + Top(cube, dir, i, _mg); half_b = base_b + Top(cube, dir, i, _mb); half_w = base_w + Top(cube, dir, i, _wt); // now half_x is sum over lower half of box, if split at i if (half_w == 0) { // subbox could be empty of pixels! continue; // never split into an empty box } else { temp = ((float)half_r * half_r + (float)half_g * half_g + (float)half_b * half_b) / half_w; } half_r = whole_r - half_r; half_g = whole_g - half_g; half_b = whole_b - half_b; half_w = whole_w - half_w; if (half_w == 0) { // subbox could be empty of pixels! continue; // never split into an empty box } else { temp += ((float)half_r * half_r + (float)half_g * half_g + (float)half_b * half_b) / half_w; } if (temp > max) { max = temp; cut = i; } } return max; } //___________________________________________ bool Cut(ref Box set1, ref Box set2) { byte dir; int cutr, cutg, cutb; int whole_r = Vol(set1, _mr); int whole_g = Vol(set1, _mg); int whole_b = Vol(set1, _mb); int whole_w = Vol(set1, _wt); float maxr = Maximize(set1, FI_RGBA_RED, set1.r0 + 1, set1.r1, out cutr, whole_r, whole_g, whole_b, whole_w); float maxg = Maximize(set1, FI_RGBA_GREEN, set1.g0 + 1, set1.g1, out cutg, whole_r, whole_g, whole_b, whole_w); float maxb = Maximize(set1, FI_RGBA_BLUE, set1.b0 + 1, set1.b1, out cutb, whole_r, whole_g, whole_b, whole_w); if ((maxr >= maxg) && (maxr >= maxb)) { dir = FI_RGBA_RED; if (cutr < 0) { return false; // can't split the box } } else if ((maxg >= maxr) && (maxg >= maxb)) { dir = FI_RGBA_GREEN; } else { dir = FI_RGBA_BLUE; } set2.r1 = set1.r1; set2.g1 = set1.g1; set2.b1 = set1.b1; switch (dir) { case FI_RGBA_RED: set2.r0 = set1.r1 = cutr; set2.g0 = set1.g0; set2.b0 = set1.b0; break; case FI_RGBA_GREEN: set2.g0 = set1.g1 = cutg; set2.r0 = set1.r0; set2.b0 = set1.b0; break; case FI_RGBA_BLUE: set2.b0 = set1.b1 = cutb; set2.r0 = set1.r0; set2.g0 = set1.g0; break; } set1.vol = (set1.r1 - set1.r0) * (set1.g1 - set1.g0) * (set1.b1 - set1.b0); set2.vol = (set2.r1 - set2.r0) * (set2.g1 - set2.g0) * (set2.b1 - set2.b0); return true; } //___________________________________________ static void Mark(in Box cube, int label, byte[] tag) { for (int r = cube.r0 + 1; r <= cube.r1; r++) { for (int g = cube.g0 + 1; g <= cube.g1; g++) { for (int b = cube.b0 + 1; b <= cube.b1; b++) { tag[FIQ_INDEX(r, g, b)] = (byte)label; } } } } [StructLayout(LayoutKind.Sequential, Pack = 2)] internal struct BITMAPFILEHEADER { public ushort bfType; public int bfSize; public ushort bfReserved1; public ushort bfReserved2; public int bfOffBits; } #pragma warning disable CS0649 //field never assigned internal struct RGBQUAD { public byte rgbBlue; public byte rgbGreen; public byte rgbRed; public byte rgbReserved; } } ================================================ FILE: Au.Editor/Tools/DCustomize.cs ================================================ using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; using Au.Controls; using ToolLand; using System.Xml.Linq; #if SCRIPT namespace Script; #endif namespace LA; class DCustomize : KDialogWindow { public static void ShowSingle(string commandName = null) { var d = ShowSingle(() => new DCustomize()); if (commandName != null) d._SelectAndOpen(commandName); } /// /// If the dialog is open, sets its Image field text and returns true. Called from the Icons dialog. /// public static bool AaSetImage(string s) { if (!GetSingle(out DCustomize d)) return false; if (d._panelProp.IsVisible) d._tImage.Text = s; return true; } List<_Custom> _tree; KSciInfoBox _info; ContextMenu _menu; Dictionary _dict = new(); KTreeView _tv; Panel _panelProp; TextBox _tColor, _tText, _tImage, _tKeys, _tBtext; TextBox _tDefText, _tDefImage, _tDefKeys; KCheckBox _cSeparator; ComboBox _cbHide, _cbImageAt; Label _lCommandPath; _Custom _ti; //current item _Custom _clip; //cut/copy item bool _ignoreEvents; DCustomize() { InitWinProp("Customize", App.Wmain); var b = new wpfBuilder(this).WinSize(700, 700).Columns(220, 0, -1); b.Row(-1); b.xAddInBorder(out _tv); b.Add().Splitter(vertical: true); b.StartGrid().Columns(-1); //right side b.Row(84).Add(out _info); b.AddSeparator(vertical: false); b.Row(-1).StartStack(vertical: true).Hidden(); _panelProp = b.Panel; b.Add(out _lCommandPath).Padding("4").Brush(Brushes.LightBlue, Brushes.Black); b.StartGrid("Properties common to menu item and toolbar button").Columns(0, -1, 30, 30); b.R.Add("Text", out _tText).Tooltip("Text.\nInsert _ before Alt-underlined character."); b.R.Add("Color", out _tColor).Tooltip("Text color.\nCan be a .NET color name or #RRGGBB or #RGB.") .xAddButtonIcon(EdIcons.Color, _ => KColorPicker.ColorTool(s => { _tColor.Text = s; }, b.Window, modal: true, add0xRgbButton: false, addBgrButton: false), "Colors").Span(1); b.R.Add("Image", out _tImage).Tooltip("Icon name etc.\nSee ImageUtil.LoadWpfImageElement.") .xAddButtonIcon(EdIcons.Icons, _ => { _tImage.SelectAll(); DIcons.ShowSingle(expandMenuIcon: true); }, "Icons tool.\nSelect an icon and click button 'Menu or toolbar item'.").Span(1); b.R.Add("Keys", out _tKeys).Tooltip("Keyboard or/and mouse shortcut(s), like Ctrl+E, Shift+M-click.\nSee keys.more.parseHotkeyString.") .xAddButtonIcon(EdIcons.Keys, _ => _KeysTool(), "Keys tool"); b.xAddButtonIcon("*FeatherIcons.Eye" + EdIcons.black, _ => _KeysList(), "Existing hotkeys"); b.StartGrid("Default"); b.R.Add("Text", out _tDefText); _Readonly(_tDefText); b.R.Add("Image", out _tDefImage); _Readonly(_tDefImage); b.R.Add("Keys", out _tDefKeys); _Readonly(_tDefKeys); static void _Readonly(TextBox k) { k.IsReadOnly = true; k.IsReadOnlyCaretVisible = true; k.Background = SystemColors.ControlBrush; } b.End(); b.End(); b.StartGrid("Toolbar button properties").Columns(0, 100, -1); b.R.Add("Text", out _tBtext).Multiline(wrap: TextWrapping.NoWrap).Tooltip("Button text, if different than menu item text."); b.R.Add("Image at", out _cbImageAt).Items("", "left", "top", "right", "bottom").Span(1).Tooltip("Display image + text and put image at this side.\nFor submenu-items always left."); b.R.Add(out _cSeparator, "Separator before"); b.R.Add("Hide", out _cbHide).Items("", "always", "never").Span(1).Tooltip("When to move the button to the overflow dropdown.\nIf empty - when the toolbar is too small."); b.End(); b.StartGrid("Move"); b.R.AddButton("Up", _ => _Move(_ti, true)).Tooltip("Shift+Up"); b.R.AddButton("Down", _ => _Move(_ti, false)).Tooltip("Shift+Down"); b.End().Width(90).Align("L"); b.End(); b.R.AddSeparator(vertical: false); b.R.StartOkCancel() .AddButton(out var bOK, "Save", _ => { _Save(); }).Width(70).Tooltip("Saves changes. Will be applied when the program starts next time.") .AddButton(out var bOK2, "Save and restart", _ => { _Save(); Close(); App.Restart(); }).Width(120).Tooltip("Saves changes and restarts the program.") .AddButton(out var bCancel, "Cancel", _ => Close()).Width(70) .End(); b.End(); //right side b.End(); _FillMenu(); _tv.SingleClickActivate = true; _tv.ItemActivated += _tv_ItemActivated; _tv.ItemClick += _tv_ItemClick; _tv.KeyDown += _tv_KeyDown; _FillTree(); foreach (var v in new Control[] { _tText, _tColor, _tImage, _tKeys, _tBtext, _cbImageAt, _cbHide, _cSeparator }) { void _Update() { if (!_ignoreEvents) { _GetControlValues(); _tv.Redraw(); } } if (v is TextBox tb) tb.TextChanged += (_, _) => _Update(); else if (v is ComboBox cb) cb.SelectionChanged += (_, _) => _Update(); else if (v is KCheckBox ch) ch.CheckChanged += (_, _) => _Update(); } b.Loaded += () => { _info.aaaText = $""" Here you can edit menus, toolbars and hotkeys of the main window. menu - customized menu items. Right-click... File, etc - toolbars. Right-click to add a button. Menu items cannot be removed or reordered. Default toolbar buttons cannot be removed, but you can edit, reorder and hide. Text color in the list: blue - customized; gray - hidden button; red - cut. You also can edit the file<> in an XML editor. To reset everything, delete the file. To reset an item or toolbar, remove it from XML. """; }; void _KeysTool() { _tKeys.SelectAll(); _tKeys.Focus(); var k = new KeysWindow { InsertInControl = _tKeys, ClickClose = KPopup.CC.Outside, CloseHides = true }; k.SetFormat(PSFormat.Hotkey); k.ShowByRect(_tKeys, Dock.Bottom); } void _KeysList() { var m = new popupMenu(); var a = new Dictionary<_Default, string>(); //add default hotkeys to a foreach (var v in _dict) { if (v.Value.attr.keys is string k) a.Add(v.Value, k); } //add custom hotkeys to a foreach (var v in _tree.SelectMany(o => o.Children())) { if (v.keys is string k) { if (k == "") a.Remove(v.def); else a[v.def] = k; } } //add all to menu foreach (var v in a.OrderBy(o => o.Value, StringComparer.OrdinalIgnoreCase)) { var u = m.Add(v.Key.text + "\t" + v.Value, o => { var d = o.Tag as _Default; var c = _tree.SelectMany(o => o.Children()).FirstOrDefault(o => o.def == d); if (c == null) { if (!dialog.showOkCancel("Customize this menu item?", d.text, owner: this)) return; _AddToCustomized(d, _tree.Find(o => o.isMenu)); } else { _SelectAndOpen(c); } }); u.Tag = v.Key; if (v.Value != v.Key.attr.keys) u.TextColor = Colors.Blue; //customized } m.Show(owner: this); } } void _tv_ItemActivated(TVItemEventArgs e) => _Open(e.Item as _Custom); void _Open(_Custom c) { //print.it(c.name); if (c.Level == 0) { _panelProp.Visibility = Visibility.Hidden; return; } _ti = c; _ignoreEvents = true; _panelProp.Visibility = Visibility.Visible; var path = new Stack(); for (var v = c.def; v != null; v = v.parent) path.Push(v.text); _lCommandPath.Content = string.Join(" > ", path); //common _tText.Text = c.ctext ?? c.def.text; _tColor.Text = c.color; _tImage.Text = c.image ?? c.def.attr.image; _tKeys.Text = c.keys ?? c.def.attr.keys; _tDefText.Text = c.def.text; _tDefImage.Text = c.def.attr.image; _tDefKeys.Text = c.def.attr.keys; //button _tBtext.Text = c.btext; _cbImageAt.SelectedIndex = c.imageAt switch { "left" => 1, "top" => 2, "right" => 3, "bottom" => 4, _ => 0 }; _cSeparator.IsChecked = c.separator; _cbHide.SelectedIndex = c.hide switch { "always" => 1, "never" => 2, _ => 0 }; _ignoreEvents = false; } void _GetControlValues() { var c = _ti; c.ctext = _Text(_tText, c.def.text); c.color = _Text(_tColor); c.image = _Text(_tImage, c.def.attr.image); c.keys = _Text(_tKeys, c.def.attr.keys); c.btext = _Text(_tBtext); c.imageAt = _Text(_cbImageAt); c.separator = _cSeparator.IsChecked; c.hide = _Text(_cbHide); string _Text(Control tbcb, string def = null) { var s = tbcb switch { TextBox tb => tb.Text, ComboBox cb => cb.SelectedValue as string, _ => null }; s = s.NullIfEmpty_(); if (def == null) return s; if (s == def) return null; return s ?? ""; } } void _SelectAndOpen(_Custom t) { _tv.SelectSingle(t); if (t != _ti) _Open(t); } void _SelectAndOpen(string commandName) { var c = _tree.SelectMany(o => o.Children()).LastOrDefault(o => o.def.name == commandName); //Last to prefer toolbar button and not menu item if (c != null) _SelectAndOpen(c); } void _tv_ItemClick(TVItemEventArgs e) { var t = e.Item as _Custom; if (e.Button == MouseButton.Right) { var m = new popupMenu(); var c0 = t.Level == 0 ? t : t.Parent; m[c0.isMenu ? "Customize menu item..." : "Add button..."] = o => { _menu.PlacementTarget = this; _menu.IsOpen = true; }; if (t.Level == 1) { m.Separator(); m["Cut"] = o => _Cut(t); _MiPaste(false); m.AddCheck("Hide", t.hide == "always", o => _Hide(t, o.IsChecked)); if (!_IsDefaultButton(t)) { m.Submenu("Delete", m => { m["Delete"] = o => _Delete(t, ask: false); }); } } else { _MiPaste(true); } void _MiPaste(bool into) { if (_CanPaste(t)) m["Paste"] = o => _Paste(t); } _contextMenuOwner = t; m.Show(owner: this); } } _Custom _contextMenuOwner; void _Cut(_Custom t) { if (_clip != null) _tv.Redraw(_clip); _clip = t; _tv.Redraw(t); //red etc } bool _CanPaste(_Custom where) { if (_clip == null || _clip == where) return false; if (_IsDefaultButton(_clip) && where != _clip.Parent && where.Parent != _clip.Parent) return false; //don't move to another toolbar return true; } void _Paste(_Custom where) { _clip.Remove(); if (where.Level == 0) where.AddChild(_clip); else where.AddSibling(_clip, after: false); _clip = null; _tv.SetItems(_tree, modified: true); } void _AddToCustomized(_Default def, _Custom where) { var c0 = where.Level == 0 ? where : where.Parent; var c = c0.Children().FirstOrDefault(o => o.def == def); if (c == null) { c = new _Custom(this, def, c0.isMenu); if (c0 == where) where.AddChild(c); else where.AddSibling(c, after: false); _tv.SetItems(_tree, modified: true); } _SelectAndOpen(c); } void _Move(_Custom t, bool up) { var where = up ? t.Previous : t.Next; if (where == null) { if (_IsDefaultButton(t)) return; //don't move to another toolbar int i = _tree.IndexOf(t.Parent) + (up ? -1 : 1); if ((uint)i >= _tree.Count) return; where = _tree[i]; } t.Remove(); if (where.Level == 0) where.AddChild(t, first: !up); else where.AddSibling(t, after: !up); _tv.SetItems(_tree, modified: true); _tv.EnsureVisible(t); } void _Delete(_Custom t, bool ask) { if (!_IsDefaultButton(t)) { if (ask && !dialog.showOkCancel("Delete?", t.displayText, owner: this)) return; if (t == _ti) _panelProp.Visibility = Visibility.Hidden; _clip = null; t.Remove(); _tv.SetItems(_tree, true); } else if (t == _ti) { _cbHide.SelectedIndex = 1; } else { t.hide = "always"; _tv.Redraw(t); } } void _Hide(_Custom t, bool hide) { if (t == _ti) { _cbHide.SelectedIndex = hide ? 1 : 0; } else { t.hide = hide ? "always" : null; _tv.Redraw(t); } } bool _IsDefaultButton(_Custom t) { if (t.Level > 0 && !t.isMenu) { _xdefault ??= XmlUtil.LoadElem(App.Commands.DefaultFile); return _xdefault.Element(t.Parent.displayText)?.Element(t.def.name) != null; } return false; } XElement _xdefault; void _FillMenu() { _Menu(Panels.Menu, _menu = new(), null, 0); void _Menu(ItemsControl sourceMenu, ItemsControl destMenu, _Default parentCommand, int level) { foreach (var o in sourceMenu.Items) { if (o is not MenuItem ms || ms.Command is not KMenuCommands.Command c) continue; string name = c.Name; var def = new _Default(name, c.Attribute, c.Text, parentCommand); _dict.Add(name, def); var m = new MenuItem { Name = name, Header = c.Text }; destMenu.Items.Add(m); if (ms.Role is MenuItemRole.SubmenuHeader or MenuItemRole.TopLevelHeader) { if (level > 0) m.PreviewMouseLeftButtonUp += (o, e) => { if (e.Source == o) { this.Focus(); _AddToCustomized(def, _contextMenuOwner); }; }; _Menu(ms, m, def, level + 1); } else { m.Click += (o, _) => _AddToCustomized(def, _contextMenuOwner); } } //} } } void _FillTree() { _tree = new(); var a = App.Commands.LoadFiles(); if (a == null) return; foreach (var xx in a) { var s1 = xx.Name.LocalName; bool isMenu = s1 == "menu"; var vv = new _Custom(this, null, isMenu, s1); _tree.Add(vv); foreach (var x in xx.Elements()) { if (!_dict.TryGetValue(x.Name.LocalName, out var def)) continue; var c = new _Custom(this, def, isMenu) { color = x.Attr("color"), ctext = x.Attr("text"), image = x.Attr("image"), keys = x.Attr("keys"), hide = x.Attr("hide"), imageAt = x.Attr("imageAt"), separator = x.HasAttr("separator"), btext = x.Attr("btext"), }; vv.AddChild(c); } } _tv.SetItems(_tree); } void _Save() { var xr = new XElement("commands"); foreach (var t in _tree) { var xt = new XElement(t.displayText); xr.Add(xt); foreach (var c in t.Children()) { if (c.isMenu && !c.IsCustomized()) continue; var x = new XElement(c.def.name); xt.Add(x); if (c.separator) x.SetAttributeValue("separator", ""); _Set("text", c.ctext); _Set("color", c.color); _Set("image", c.image); _Set("keys", c.keys); _Set("btext", c.btext); _Set("imageAt", c.imageAt); _Set("hide", c.hide); void _Set(string attr, string s) { if (s != null) x.SetAttributeValue(attr, s); } } } //print.clear(); //print.it(xr); xr.SaveElem(App.Commands.UserFile); } //default properties (Menus member name and attributes) record _Default(string name, CommandAttribute attr, string text, _Default parent); //customized properties (in XML file) class _Custom : TreeBase<_Custom>, ITreeViewItem { readonly DCustomize _d; public readonly _Default def; public string displayText; public readonly bool isMenu; bool _isExpanded; //customized properties public bool separator; public string ctext, color, image, keys, btext, imageAt, hide; public _Custom(DCustomize d, _Default def, bool isMenu, string displayText = null) { _d = d; this.def = def; this.isMenu = isMenu; this.displayText = displayText ?? StringUtil.RemoveUnderlineChar(def.text, '_'); _isExpanded = def == null; } #region ITreeViewItem void ITreeViewItem.SetIsExpanded(bool yes) { _isExpanded = yes; } public bool IsExpanded => _isExpanded; IEnumerable ITreeViewItem.Items => base.Children(); public bool IsFolder => base.HasChildren; string ITreeViewItem.DisplayText => displayText; object ITreeViewItem.Image => Level == 0 ? EdIcons.FolderArrow(_isExpanded) : (image ?? def.attr.image); int ITreeViewItem.Color(TVColorInfo ci) => Level > 0 ? -1 : isMenu ? 0xF0C080 : 0xC0E0A0; int ITreeViewItem.TextColor(TVColorInfo ci) => this == _d._clip ? 0xFF0000 : hide == "always" ? 0x808080 : IsCustomized() ? (ci.isHighContrastDark ? 0xFFFF00 : 0x0000FF) : Level == 0 ? 0 : -1; #endregion public bool IsCustomized() { if (def == null) return false; if (separator && isMenu) return true; //never mind: or may be specified in the default XML file, but that info now is lost. return ctext != null || color != null || image != null || keys != null || btext != null || imageAt != null || hide != null; } } void _tv_KeyDown(object sender, KeyEventArgs e) { var k = (e.KeyboardDevice.Modifiers, e.Key); switch (k) { case (0, Key.Escape): if (_clip != null) { _clip = null; _tv.Redraw(); } goto gh; } if (_tv.FocusedItem is _Custom t) { switch (k) { case (0, Key.Delete): if (t.Level > 0) _Delete(t, ask: true); break; case (ModifierKeys.Control, Key.X): if (t.Level > 0) _Cut(t); break; case (ModifierKeys.Control, Key.V): if (_CanPaste(t)) _Paste(t); break; case (ModifierKeys.Shift, Key.Up): if (t.Level > 0) _Move(t, up: true); break; case (ModifierKeys.Shift, Key.Down): if (t.Level > 0) _Move(t, up: false); break; default: return; } } gh: e.Handled = true; } public static void ToolbarContextMenuOpening(object sender, ContextMenuEventArgs _1) { if (GetSingle(out _)) return; var toolbar = sender as ToolBar; FrameworkElement e = null; ICommand ic = null; int index = 0; //WPF does not have a better way to get the right-clicked element when it is disabled. Mouse.DirectlyOver etc return the container. var xy = mouse.xy; foreach (FrameworkElement v in toolbar.Items) { if (v.IsVisible && v is System.Windows.Controls.Primitives.ButtonBase or Border && v.RectInScreen().Contains(xy)) { e = v; if (v is ButtonBase b1) ic = b1.Command; else if (v is Border b2 && b2.Child is Menu m2) ic = m2.Items.OfType().SingleOrDefault()?.Command; break; } index++; } if (ic is not KMenuCommands.Command c) return; OverflowMode hide1 = ToolBar.GetOverflowMode(e); bool separatorBefore = index > 0 && toolbar.Items[index - 1] is Separator; var m = new popupMenu(); m["Customize..."] = o => DCustomize.ShowSingle(c.Name); m.Separator(); m.AddCheck("Hide", hide1 == OverflowMode.Always, _ => _Hide(hide1 == OverflowMode.Always ? OverflowMode.AsNeeded : OverflowMode.Always)); m.AddCheck("Never hide", hide1 == OverflowMode.Never, _ => _Hide(hide1 == OverflowMode.Never ? OverflowMode.AsNeeded : OverflowMode.Never)); if (!ToolBar.GetIsOverflowItem(e)) { m.Separator(); m.AddCheck("Separator before", separatorBefore, o => _Separator()); } m.Show(); void _Hide(OverflowMode hide2) { if (!_ModifyUserXmlButton(x => { x.SetAttributeValue("hide", hide2 switch { OverflowMode.Always => "always", OverflowMode.Never => "never", _ => null }); })) return; ToolBar.SetOverflowMode(e, hide2); if (separatorBefore) ToolBar.SetOverflowMode(toolbar.Items[index - 1] as Separator, hide2); } void _Separator() { if (!_ModifyUserXmlButton(x => { x.SetAttributeValue("separator", separatorBefore ? null : ""); })) return; if (separatorBefore) { toolbar.Items.RemoveAt(index - 1); } else { var sep = new Separator(); if (hide1 != default) ToolBar.SetOverflowMode(sep, hide1); toolbar.Items.Insert(index, sep); } } bool _ModifyUserXmlButton(Action modify) { if (App.Commands.LoadFiles() is not { } toolbars) return false; if (toolbars.FirstOrDefault(o => o.Name == toolbar.Name)?.Element(c.Name) is not { } x) return false; modify(x); new XElement("commands", toolbars).SaveElem(App.Commands.UserFile); return true; } } } ================================================ FILE: Au.Editor/Tools/DCustomizeContextMenu.cs ================================================ using Au.Controls; using System.Windows.Controls; using System.Windows; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; namespace LA; static class DCustomizeContextMenu { public static void Dialog(string menuName, string ownerName) { var (file, text) = _GetFilePathAndText(menuName); var b = new wpfBuilder("Customize context menu").WinSize(550, 550).Columns(-1, -1); b.WinProperties(showInTaskbar: false); b.Row(40).Add(out KSciInfoBox info); info.aaaText = $""" Left - all menu commands. Select a line and drag or copy to the right. Right - commands to add to the context menu of the {ownerName}. Separator -. """; b.Row(-1).xAddInBorder(out KScintilla t1); t1.AaInitReadOnlyAlways = true; b.Loaded += () => { t1.aaaText = _GetAllCommands(menuName, out int goTo); if (goTo > 0) t1.Call(Sci.SCI_LINESCROLL, 0, t1.aaaLineFromPos(false, goTo)); }; b.xAddInBorder(out KScintilla t2); t2.AaInitUseDefaultContextMenu = true; t2.aaaText = text; //b.Validation(o => never mind); t2.AaNotify += e => { unsafe { if (e.n.code == Sci.NOTIF.SCN_MODIFIED) { //trim tabs if (e.n.modificationType.Has(Sci.MOD.SC_MOD_INSERTCHECK) && e.n.length > 1 && e.n.textUTF8[0] == 9) { var s = e.n.Text.RxReplace(@"(?m)^\t+", ""); e.c.aaaSetString(Sci.SCI_CHANGEINSERTION, s); } } } }; b.R.AddOkCancel(); b.End(); if (!b.ShowDialog(App.Wmain)) return; filesystem.saveText(file, t2.aaaText); } static string _GetAllCommands(string menuName, out int goTo) { int go = -1; var b = new StringBuilder(); _Menu(Panels.Menu, 0); goTo = go; return b.ToString(); void _Menu(ItemsControl sourceMenu, int level) { foreach (var o in sourceMenu.Items) { if (o is not MenuItem ms || ms.Command is not KMenuCommands.Command c) continue; bool isMenu = ms.Role is MenuItemRole.SubmenuHeader or MenuItemRole.TopLevelHeader; if (isMenu && go < 0 && c.Name == menuName) go = b.Length; b.Append('\t', level).AppendLine(c.Name); if (isMenu) _Menu(ms, level + 1); } } } static (string path, string text) _GetFilePathAndText(string menuName) { string file = AppSettings.DirBS + menuName + " context menu.txt", text = null; if (filesystem.exists(file)) text = filesystem.loadText(file); else if (menuName == "Edit") text = """ Cut Copy Paste - Surround Toggle_bookmark """; return (file, text); } public static void AddToMenu(KWpfMenu m, string menuName) { try { if (_GetFilePathAndText(menuName).text is not string text) return; var a = text.Lines(); bool needSeparator = false; foreach (var line in a) { var s = line.Trim(); if (s.Length == 0 || s.Starts('/')) continue; if (s == "-") { m.Separator(); needSeparator = false; } else if (App.Commands.TryFind(s, out var c)) { var k = new MenuItem(); c.CopyToMenu(k); m.Items.Add(k); needSeparator = true; } else { //print.it($"<>Unknown menu command: {s}."); } } if (needSeparator) m.Items.Add(new Separator()); } catch (Exception e1) { Debug_.Print(e1); } } } ================================================ FILE: Au.Editor/Tools/DEnumFiles.cs ================================================ using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; using System.Windows.Data; using Au.Controls; using ToolLand; namespace LA; class DEnumDir : KDialogWindow { public static void Dialog(string initFolder = null) { new DEnumDir(initFolder).Show(); } KTextBoxFile _tFolder; ComboBox _cbGet; KTextExpressionBox _tFilter; KCheckBox _cArray, _cForeach, _cFileFilter, _cDirFilter, _cRecurse, _cIgnore, _cSymlink, _cRelative, _cSkipHidden, _cSkipHiddenSystem, _cWhere, _cOrderBy, _cThenBy, _cSelectPath, _cSelectOther/*, _cNet*/; Panel _p1, _p2; KSciCodeBox _code; const string c_Where = ".Where(o => o.EditMe > EditMe)", c_OrderBy = ".OrderBy(o => o.EditMe)", c_ThenBy = ".ThenBy(o => o.EditMe)", c_Select = ".Select(o => o.EditMe)"; DEnumDir(string initFolder = null) { InitWinProp("Get files in folder", App.Wmain); _noeventValueChanged = true; var b = new wpfBuilder(this).WinSize(600).Columns(0, 90, 90, -1); b.R.StartGrid().Columns(76, 76, 0, 0, -1); CancellationTokenSource cts = null; b.AddButton("Test", async e => { if (cts == null) { var s = _FormatCode(true); print.clear(); e.Button.Content = "Stop"; cts = new(); var r = await TUtil.RunTestCodeAsync(s, cts.Token); cts = null; if (!IsLoaded) return; e.Button.Content = "Test"; if (r?.isError == true) print.it($"<>{r.header}<>\r\n{r.text}"); } else { cts.Cancel(); } }); Closing += (_, _) => { cts?.Cancel(); }; b.AddButton("OK", _ => { Close(); var s = _FormatCode(); InsertCode.Statements(new(s, makeVarName1: true)); }); b.Add(out _cArray, "Get array").Checked().Tooltip("Store results in an array variable before starting to use them.\nThen it is safe to delete/rename/move/copy the files.\nBut then cannot start using results until all retrieved from the file system.\nCheck if going to delete/rename/move/copy files.\nUncheck if going to search in a potentially large directory.") .Add(out _cForeach, "Use foreach").Checked(); //b.Add(out _cNet, "Use only .NET classes"); b.End(); b.R.AddSeparator(); b.R.Add("Folder", out _tFolder, initFolder).Tooltip("Folder path.\nPaste, drop or right-click.").Focus(); _tFolder.IsFolder = true; _tFolder.Unexpand = App.Settings.tools_pathUnexpand; b.R.Add("Get", out _cbGet).Items("files|folders|all").Margin("R12"); _cbGet.SelectionChanged += (_, _) => { _p1.Visibility = _cbGet.SelectedIndex == 2 ? Visibility.Hidden : Visibility.Visible; _p2.Visibility = _cbGet.SelectedIndex == 2 ? Visibility.Visible : Visibility.Hidden; }; b.StartGrid().Columns(0, -1, 0); _p1 = b.Panel; b.Add("Name wildex", out _tFilter).Tooltip("Optional file name wildex.\nExamples:\n*.txt\n**m *.png||*.jpg") .xAddControlHelpButton("articles/Wildcard expression", "Wildcard expression help"); b.End().Span(2); b.And(0).StartStack(); _p2 = b.Panel; b.Add(out _cFileFilter, "Use file filter").Add(out _cDirFilter, "Use folder filter").Margin("L8"); b.End().Hidden(); b.R.StartGrid("Flags"); b.R.Add(out _cRecurse, "Include subfolders"); b.R.Add(out _cIgnore, "Ignore inaccessible").Margin("L22").xBindCheckedVisible(_cRecurse); b.R.Add(out _cSymlink, "Follow NTFS links").Margin("L22").xBindCheckedVisible(_cRecurse).Tooltip("Enumerate the target folder of NTFS links (symbolic links, volume mount points, etc)"); b.R.Add(out _cRelative, "Get relative path").Margin("L22").xBindCheckedVisible(_cRecurse).Tooltip("Store relative path in the Name property"); b.R.Add(out _cSkipHidden, "Skip hidden"); b.R.Add(out _cSkipHiddenSystem, "Skip hidden system"); b.End().Span(3); b.StartGrid("Results"); b.R.Add(out _cWhere, "Where") .Tooltip($"Add code '{c_Where}' to filter results by size, date, etc.\nThen in the code editor you'll edit it."); b.R.StartStack(); b.Add(out _cOrderBy, "OrderBy") .Tooltip($"Add code '{c_OrderBy}' to sort results by some property.\nThen in the code editor you'll edit it. You can replace OrderBy with OrderByDescending."); b.Add(out _cThenBy, "ThenBy").xBindCheckedVisible(_cOrderBy) .Tooltip($"Adds code '{c_ThenBy}' to sort results by another property.\nThen in the code editor you'll edit it. You can replace ThenBy with ThenByDescending, and append more ThenBy/ThenByDescending if need."); b.End(); b.R.StartStack(); b.Add(out _cSelectPath, "Select path") .Tooltip($"Get only paths, not all properties."); b.Add(out _cSelectOther, "Select other") .Tooltip($"Add code '{c_Select}' to change the type of results.\nFor example select a property or several properties (tuple).\nThen in the code editor you'll edit it."); b.End(); b.End(); b.R.AddSeparator(); b.Row(150..).xAddInBorder(out _code); b.End(); _noeventValueChanged = false; b.Loaded += () => { _FormatCode(); }; _tFolder.UnexpandChanged += () => { App.Settings.tools_pathUnexpand = _tFolder.Unexpand; _FormatCode(); }; } static DEnumDir() { TUtil.OnAnyCheckTextBoxValueChanged((d, o) => d._AnyCheckTextBoxComboValueChanged(o), comboToo: true); } //when checked/unchecked any checkbox, and when text changed of any textbox or combobox void _AnyCheckTextBoxComboValueChanged(object source) { if (!_noeventValueChanged) { _noeventValueChanged = true; if (source == _cSkipHidden && _cSkipHidden.IsChecked) _cSkipHiddenSystem.IsChecked = false; if (source == _cSkipHiddenSystem && _cSkipHiddenSystem.IsChecked) _cSkipHidden.IsChecked = false; _noeventValueChanged = false; _FormatCode(); } } bool _noeventValueChanged; string _FormatCode(bool forTest = false, bool onOK = false) { int getWhat = _cbGet.SelectedIndex; string filter = _tFilter.Text; bool hasFilter = getWhat != 2 && !filter.NE(); var b = new StringBuilder(); b.AppendLine($"var folder = {_tFolder.GetCode()};"); b.Append("var a = "); //if (_cNet.IsChecked) { //} else { b.Append("filesystem.").Append(getWhat switch { 0 => "enumFiles", 1 => "enumDirectories", _ => "enumerate" }); b.Append("(folder"); if (hasFilter) b.AppendStringArg(filter); bool rec = _cRecurse.IsChecked; if (rec || _cSkipHidden.IsChecked || _cSkipHiddenSystem.IsChecked) { if (TUtil.FormatFlags(out var s1, (rec, FEFlags.AllDescendants), (rec && _cIgnore.IsChecked, FEFlags.IgnoreInaccessible), (rec && _cSymlink.IsChecked, FEFlags.RecurseNtfsLinks), (rec && _cRelative.IsChecked, FEFlags.NeedRelativePaths), (_cSkipHidden.IsChecked, FEFlags.SkipHidden), (_cSkipHiddenSystem.IsChecked, FEFlags.SkipHiddenSystem) )) b.AppendOtherArg(s1, hasFilter ? null : "flags"); } if (getWhat == 2 && !forTest) { if (_cFileFilter.IsChecked) b.AppendOtherArg("""f => f.Name.Ends(".example", true)"""); if (_cDirFilter.IsChecked) b.AppendOtherArg("""d => d.Name.Eqi("Example") ? 0 : 3"""); //rejected: errorHandler parameter. } b.Append(')'); if (!forTest) { int len1 = b.Length; if (_cWhere.IsChecked) b.Append("\r\n\t").Append(c_Where); if (_cOrderBy.IsChecked) { b.Append("\r\n\t").Append(c_OrderBy); if (_cThenBy.IsChecked) b.Append(c_ThenBy); } if (_cSelectOther.IsChecked) b.Append("\r\n\t").Append(_cSelectPath.IsChecked ? ".Select(o => (path: o.FullPath, other: o.EditMe))" : c_Select); else if (_cSelectPath.IsChecked) b.Append("\r\n\t.Select(o => o.FullPath)"); if (b.Length > len1) b.Append("\r\n\t"); } //} b.Append(_cArray.IsChecked && !forTest ? ".ToArray();" : ";"); if (_cForeach.IsChecked || forTest) { b.Append("\r\nforeach "); if (!forTest && (_cSelectPath.IsChecked || _cSelectOther.IsChecked)) b.Append("(var f in a) {\r\n\tprint.it(f);"); else b.Append("(var f in a) {\r\n\tvar path = f.FullPath;\r\n\tprint.it(path);"); b.Append("\r\n\t"); if (forTest) b.Append("if (cancel.IsCancellationRequested) break;"); b.Append("\r\n}"); } var R = b.ToString(); if (!(forTest | onOK)) _code.AaSetText(R); return R; } } ================================================ FILE: Au.Editor/Tools/DIcons.cs ================================================ using System.Security.Authentication; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using Au.Controls; namespace LA; class DIcons : KDialogWindow { public static void ShowSingle(string find = null, bool expandFileIcon = false, bool expandMenuIcon = false) { var d = ShowSingle(() => new DIcons(randomizeColors: find == null, expandFileIcon, expandMenuIcon)); if (find != null) d._tName.Text = find; } enum _Action { FileIcon = 1, MenuIcon, //InsertXamlVar, //InsertXamlField, CopyName, CopyXaml, ExportXaml, ExportIcon, _DebugCopyPng, } List<_Item> _a; string _color = "#000000"; Random _random; int _dpi; CancellationTokenSource _ctsWindow = new(), _ctsTask; KTreeView _tv; KTextBox _tName; KScintilla _tCustom; DIcons(bool randomizeColors, bool expandFileIcon, bool expandMenuIcon) { InitWinProp("Icons", App.Wmain); var b = new wpfBuilder(this).WinSize(600, 600); b.Columns(-1, 0); //left - edit control and tree view b.Row(-1).StartGrid().Columns(-1, 0); b.Add(out _tName).Focus().Tooltip(@"Search. Part of icon name, or wildcard expression. Examples: part, Part (match case), start*, *end, **rc regex case-sensitive. Can be Pack.Icon, like Material.Folder."); _tName.PreviewKeyDown += (_, e) => { if (e.Key == Key.Enter) _AiSearch(); }; b.xAddButtonIcon(EdIcons.AiSearch, _ => _AiSearch(), "Use AI to find icons.\nCan search by name or/and image.\nType what you want in the text box or/and copy an icon image (for example in web browser, PNG format). Then click this button."); b.Row(-1).xAddInBorder(out _tv); _tv.ImageBrush = System.Drawing.Brushes.White; b.End(); //right - color picker, buttons, etc b.StartGrid().Columns(-1); b.Add(out KColorPicker colors).Align(HorizontalAlignment.Left); colors.ColorChanged += color => { _random = null; _color = _ColorToString(color); _tv.Redraw(); }; b.StartStack(); TextBox randFromTo = null, iconSizes = null; b.AddButton("Randomize colors", _ => _RandomizeColors()); b.Add("L %", out randFromTo, "30-70").Width(50); b.End(); b.AddSeparator().Margin("B20"); //rejected: double-clicking an icon clicks the last clicked button. Unclear and not so useful. b.AlsoAll((b, e) => { if (b.Last is Expander er) er.Header = new TextBlock { Text = er.Header as string, FontWeight = FontWeights.Bold }; }); b.StartGrid(out var exp1, "Set file icon").Columns(0, 70, 70, -1); b.R.Add("File", out ComboBox cbIconOf).Items("Selected file(s)", "Script files", "Class files", "Folders", "Open folders").Span(2); b.R.Add