├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── dotnet-desktop.yml ├── .gitignore ├── CenterTaskbar.sln ├── CenterTaskbar ├── CenterTaskbar.csproj ├── DisplaySettings.cs ├── Program.cs ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ └── Resources.resx ├── Resources │ └── TrayIcon.ico └── TrayApplication.cs ├── License.md └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Windows 11 will not be supported, please do not make another issue regarding this 11 | 12 | **Describe the bug** 13 | A clear and concise description of what the bug is. 14 | 15 | **To Reproduce** 16 | Steps to reproduce the behavior: 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | **Expected behavior** 23 | A clear and concise description of what you expected to happen. 24 | 25 | **Screenshots** 26 | If applicable, add screenshots to help explain your problem. 27 | 28 | **Desktop (please complete the following information):** 29 | - OS: [e.g. iOS] 30 | - Browser [e.g. chrome, safari] 31 | - Version [e.g. 22] 32 | 33 | **Smartphone (please complete the following information):** 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Not accepting at this time 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Feature requests must be in the form of a pull request as the project is not in active development and will likely be closed 11 | 12 | Windows 11 will not be supported, please do not make another issue regarding this 13 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-desktop.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # This workflow will build, test, sign and package a WPF or Windows Forms desktop application 7 | # built on .NET Core. 8 | # To learn how to migrate your existing application to .NET Core, 9 | # refer to https://docs.microsoft.com/en-us/dotnet/desktop-wpf/migration/convert-project-from-net-framework 10 | # 11 | # To configure this workflow: 12 | # 13 | # 1. Configure environment variables 14 | # GitHub sets default environment variables for every workflow run. 15 | # Replace the variables relative to your project in the "env" section below. 16 | # 17 | # 2. Signing 18 | # Generate a signing certificate in the Windows Application 19 | # Packaging Project or add an existing signing certificate to the project. 20 | # Next, use PowerShell to encode the .pfx file using Base64 encoding 21 | # by running the following Powershell script to generate the output string: 22 | # 23 | # $pfx_cert = Get-Content '.\SigningCertificate.pfx' -Encoding Byte 24 | # [System.Convert]::ToBase64String($pfx_cert) | Out-File 'SigningCertificate_Encoded.txt' 25 | # 26 | # Open the output file, SigningCertificate_Encoded.txt, and copy the 27 | # string inside. Then, add the string to the repo as a GitHub secret 28 | # and name it "Base64_Encoded_Pfx." 29 | # For more information on how to configure your signing certificate for 30 | # this workflow, refer to https://github.com/microsoft/github-actions-for-desktop-apps#signing 31 | # 32 | # Finally, add the signing certificate password to the repo as a secret and name it "Pfx_Key". 33 | # See "Build the Windows Application Packaging project" below to see how the secret is used. 34 | # 35 | # For more information on GitHub Actions, refer to https://github.com/features/actions 36 | # For a complete CI/CD sample to get started with GitHub Action workflows for Desktop Applications, 37 | # refer to https://github.com/microsoft/github-actions-for-desktop-apps 38 | 39 | name: .NET Core Desktop 40 | 41 | on: 42 | push: 43 | branches: [ master ] 44 | pull_request: 45 | branches: [ master ] 46 | 47 | jobs: 48 | 49 | build: 50 | 51 | strategy: 52 | matrix: 53 | configuration: [Release] 54 | 55 | runs-on: windows-latest # For a list of available runner types, refer to 56 | # https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on 57 | 58 | env: 59 | Solution_Name: CenterTaskbar.sln # Replace with your solution name, i.e. MyWpfApp.sln. 60 | # Test_Project_Path: your-test-project-path # Replace with the path to your test project, i.e. MyWpfApp.Tests\MyWpfApp.Tests.csproj. 61 | Wap_Project_Directory: CenterTaskbar # Replace with the Wap project directory relative to the solution, i.e. MyWpfApp.Package. 62 | Wap_Project_Path: CenterTaskbar\CenterTaskbar.csproj # Replace with the path to your Wap project, i.e. MyWpf.App.Package\MyWpfApp.Package.wapproj. 63 | 64 | steps: 65 | - name: Checkout 66 | uses: actions/checkout@v2 67 | with: 68 | fetch-depth: 0 69 | 70 | # Install the .NET Core workload 71 | - name: Install .NET Core 72 | uses: actions/setup-dotnet@v1 73 | with: 74 | dotnet-version: 5.0.x 75 | 76 | # Add MSBuild to the PATH: https://github.com/microsoft/setup-msbuild 77 | - name: Setup MSBuild.exe 78 | uses: microsoft/setup-msbuild@v1.0.2 79 | 80 | # Execute all unit tests in the solution 81 | # - name: Execute unit tests 82 | # run: dotnet test 83 | 84 | # Restore the application to populate the obj folder with RuntimeIdentifiers 85 | - name: Restore the application 86 | run: msbuild $env:Solution_Name /t:Restore /p:Configuration=$env:Configuration 87 | env: 88 | Configuration: ${{ matrix.configuration }} 89 | 90 | # Decode the base 64 encoded pfx and save the Signing_Certificate 91 | # - name: Decode the pfx 92 | # run: | 93 | # $pfx_cert_byte = [System.Convert]::FromBase64String("${{ secrets.Base64_Encoded_Pfx }}") 94 | # $certificatePath = Join-Path -Path $env:Wap_Project_Directory -ChildPath GitHubActionsWorkflow.pfx 95 | # [IO.File]::WriteAllBytes("$certificatePath", $pfx_cert_byte) 96 | 97 | # Create the app package by building and packaging the Windows Application Packaging project 98 | - name: Create the app package 99 | # run: msbuild $env:Wap_Project_Path /p:Configuration=$env:Configuration /p:UapAppxPackageBuildMode=$env:Appx_Package_Build_Mode /p:AppxBundle=$env:Appx_Bundle /p:PackageCertificateKeyFile=GitHubActionsWorkflow.pfx /p:PackageCertificatePassword=${{ secrets.Pfx_Key }} 100 | run: | 101 | msbuild $env:Wap_Project_Path /p:Configuration=$env:Configuration # /p:UapAppxPackageBuildMode=$env:Appx_Package_Build_Mode /p:AppxBundle=$env:Appx_Bundle 102 | dotnet publish -c Release -r win10-x64 -p:PublishSingleFile=true --no-self-contained 103 | env: 104 | Appx_Bundle: Always 105 | Appx_Bundle_Platforms: x86|x64 106 | Appx_Package_Build_Mode: StoreUpload 107 | Configuration: ${{ matrix.configuration }} 108 | 109 | # Remove the pfx 110 | # - name: Remove the pfx 111 | # run: Remove-Item -path $env:Wap_Project_Directory\$env:Signing_Certificate 112 | 113 | # Upload 114 | - name: Upload build artifacts 115 | uses: actions/upload-artifact@v2 116 | with: 117 | name: CenterTaskbar.${{ matrix.configuration }} 118 | path: ${{ env.Wap_Project_Directory }}\bin\${{ matrix.configuration }}\net5.0-windows\win10-x64\publish\CenterTaskbar.exe 119 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Nuget personal access tokens and Credentials 210 | nuget.config 211 | 212 | # Microsoft Azure Build Output 213 | csx/ 214 | *.build.csdef 215 | 216 | # Microsoft Azure Emulator 217 | ecf/ 218 | rcf/ 219 | 220 | # Windows Store app package directories and files 221 | AppPackages/ 222 | BundleArtifacts/ 223 | Package.StoreAssociation.xml 224 | _pkginfo.txt 225 | *.appx 226 | *.appxbundle 227 | *.appxupload 228 | 229 | # Visual Studio cache files 230 | # files ending in .cache can be ignored 231 | *.[Cc]ache 232 | # but keep track of directories ending in .cache 233 | !?*.[Cc]ache/ 234 | 235 | # Others 236 | ClientBin/ 237 | ~$* 238 | *~ 239 | *.dbmdl 240 | *.dbproj.schemaview 241 | *.jfm 242 | *.pfx 243 | *.publishsettings 244 | orleans.codegen.cs 245 | 246 | # Including strong name files can present a security risk 247 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 248 | #*.snk 249 | 250 | # Since there are multiple workflows, uncomment next line to ignore bower_components 251 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 252 | #bower_components/ 253 | 254 | # RIA/Silverlight projects 255 | Generated_Code/ 256 | 257 | # Backup & report files from converting an old project file 258 | # to a newer Visual Studio version. Backup files are not needed, 259 | # because we have git ;-) 260 | _UpgradeReport_Files/ 261 | Backup*/ 262 | UpgradeLog*.XML 263 | UpgradeLog*.htm 264 | ServiceFabricBackup/ 265 | *.rptproj.bak 266 | 267 | # SQL Server files 268 | *.mdf 269 | *.ldf 270 | *.ndf 271 | 272 | # Business Intelligence projects 273 | *.rdl.data 274 | *.bim.layout 275 | *.bim_*.settings 276 | *.rptproj.rsuser 277 | *- [Bb]ackup.rdl 278 | *- [Bb]ackup ([0-9]).rdl 279 | *- [Bb]ackup ([0-9][0-9]).rdl 280 | 281 | # Microsoft Fakes 282 | FakesAssemblies/ 283 | 284 | # GhostDoc plugin setting file 285 | *.GhostDoc.xml 286 | 287 | # Node.js Tools for Visual Studio 288 | .ntvs_analysis.dat 289 | node_modules/ 290 | 291 | # Visual Studio 6 build log 292 | *.plg 293 | 294 | # Visual Studio 6 workspace options file 295 | *.opt 296 | 297 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 298 | *.vbw 299 | 300 | # Visual Studio LightSwitch build output 301 | **/*.HTMLClient/GeneratedArtifacts 302 | **/*.DesktopClient/GeneratedArtifacts 303 | **/*.DesktopClient/ModelManifest.xml 304 | **/*.Server/GeneratedArtifacts 305 | **/*.Server/ModelManifest.xml 306 | _Pvt_Extensions 307 | 308 | # Paket dependency manager 309 | .paket/paket.exe 310 | paket-files/ 311 | 312 | # FAKE - F# Make 313 | .fake/ 314 | 315 | # CodeRush personal settings 316 | .cr/personal 317 | 318 | # Python Tools for Visual Studio (PTVS) 319 | __pycache__/ 320 | *.pyc 321 | 322 | # Cake - Uncomment if you are using it 323 | # tools/** 324 | # !tools/packages.config 325 | 326 | # Tabs Studio 327 | *.tss 328 | 329 | # Telerik's JustMock configuration file 330 | *.jmconfig 331 | 332 | # BizTalk build output 333 | *.btp.cs 334 | *.btm.cs 335 | *.odx.cs 336 | *.xsd.cs 337 | 338 | # OpenCover UI analysis results 339 | OpenCover/ 340 | 341 | # Azure Stream Analytics local run output 342 | ASALocalRun/ 343 | 344 | # MSBuild Binary and Structured Log 345 | *.binlog 346 | 347 | # NVidia Nsight GPU debugger configuration file 348 | *.nvuser 349 | 350 | # MFractors (Xamarin productivity tool) working folder 351 | .mfractor/ 352 | 353 | # Local History for Visual Studio 354 | .localhistory/ 355 | 356 | # BeatPulse healthcheck temp database 357 | healthchecksdb 358 | 359 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 360 | MigrationBackup/ 361 | 362 | # Ionide (cross platform F# VS Code tools) working folder 363 | .ionide/ 364 | 365 | # Fody - auto-generated XML schema 366 | FodyWeavers.xsd 367 | 368 | # VS Code files for those working on multiple tools 369 | .vscode/* 370 | !.vscode/settings.json 371 | !.vscode/tasks.json 372 | !.vscode/launch.json 373 | !.vscode/extensions.json 374 | *.code-workspace 375 | 376 | # Local History for Visual Studio Code 377 | .history/ 378 | 379 | # Windows Installer files from build outputs 380 | *.cab 381 | *.msi 382 | *.msix 383 | *.msm 384 | *.msp 385 | 386 | # JetBrains Rider 387 | .idea/ 388 | *.sln.iml 389 | -------------------------------------------------------------------------------- /CenterTaskbar.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.136 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CenterTaskbar", "CenterTaskbar\CenterTaskbar.csproj", "{939EFB11-A324-4C2C-8E16-E6529B3B0FF4}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {939EFB11-A324-4C2C-8E16-E6529B3B0FF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {939EFB11-A324-4C2C-8E16-E6529B3B0FF4}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {939EFB11-A324-4C2C-8E16-E6529B3B0FF4}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {939EFB11-A324-4C2C-8E16-E6529B3B0FF4}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {3E72CEDC-81F7-4ECE-BCE6-3B02F19B30C7} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /CenterTaskbar/CenterTaskbar.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net5.0-windows 4 | WinExe 5 | true 6 | true 7 | Resources\TrayIcon.ico 8 | Copyright © 2018 9 | 1.1.0 10 | 11 | 12 | 13 | True 14 | True 15 | Resources.resx 16 | 17 | 18 | 19 | 20 | ResXFileCodeGenerator 21 | Resources.Designer.cs 22 | 23 | 24 | -------------------------------------------------------------------------------- /CenterTaskbar/DisplaySettings.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using System.Windows.Forms; 3 | 4 | namespace CenterTaskbar 5 | { 6 | internal static class DisplaySettings 7 | { 8 | 9 | [DllImport("user32.dll")] 10 | private static extern bool EnumDisplaySettings(string deviceName, int modeNum, ref DEVMODE devMode); 11 | const int ENUM_CURRENT_SETTINGS = -1; 12 | const int ENUM_REGISTRY_SETTINGS = -2; 13 | 14 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] 15 | private struct DEVMODE 16 | { 17 | private const int CCHDEVICENAME = 32; 18 | private const int CCHFORMNAME = 32; 19 | [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCHDEVICENAME)] 20 | public string dmDeviceName; 21 | public short dmSpecVersion; 22 | public short dmDriverVersion; 23 | public short dmSize; 24 | public short dmDriverExtra; 25 | public int dmFields; 26 | public int dmPositionX; 27 | public int dmPositionY; 28 | public ScreenOrientation dmDisplayOrientation; 29 | public int dmDisplayFixedOutput; 30 | public short dmColor; 31 | public short dmDuplex; 32 | public short dmYResolution; 33 | public short dmTTOption; 34 | public short dmCollate; 35 | [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCHFORMNAME)] 36 | public string dmFormName; 37 | public short dmLogPixels; 38 | public int dmBitsPerPel; 39 | public int dmPelsWidth; 40 | public int dmPelsHeight; 41 | public int dmDisplayFlags; 42 | public int dmDisplayFrequency; 43 | public int dmICMMethod; 44 | public int dmICMIntent; 45 | public int dmMediaType; 46 | public int dmDitherType; 47 | public int dmReserved1; 48 | public int dmReserved2; 49 | public int dmPanningWidth; 50 | public int dmPanningHeight; 51 | } 52 | 53 | //public static void ListAllDisplayModes() 54 | //{ 55 | // DEVMODE vDevMode = new DEVMODE(); 56 | // int i = 0; 57 | // while (EnumDisplaySettings(null, i, ref vDevMode)) 58 | // { 59 | // Console.WriteLine("Width:{0} Height:{1} Color:{2} Frequency:{3}", 60 | // vDevMode.dmPelsWidth, 61 | // vDevMode.dmPelsHeight, 62 | // 1 << vDevMode.dmBitsPerPel, vDevMode.dmDisplayFrequency 63 | // ); 64 | // i++; 65 | // } 66 | //} 67 | 68 | public static int CurrentRefreshRate() 69 | { 70 | var vDevMode = new DEVMODE(); 71 | return EnumDisplaySettings(null, ENUM_CURRENT_SETTINGS, ref vDevMode) ? vDevMode.dmDisplayFrequency : 60; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /CenterTaskbar/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Runtime.InteropServices; 4 | using System.Threading; 5 | using System.Windows.Forms; 6 | 7 | namespace CenterTaskbar 8 | { 9 | internal static class Program 10 | { 11 | /// 12 | /// The main entry point for the application. 13 | /// 14 | [STAThread] 15 | private static void Main(string[] args) 16 | { 17 | // Only allow one instance of this application to run at a time using GUID 18 | var assemblyGuid = Assembly.GetExecutingAssembly().GetCustomAttribute().Value.ToUpper(); 19 | using (new Mutex(true, assemblyGuid, out var firstInstance)) 20 | { 21 | if (!firstInstance) 22 | { 23 | MessageBox.Show("Another instance is already running.", "CenterTaskbar", MessageBoxButtons.OK, 24 | MessageBoxIcon.Exclamation); 25 | return; 26 | } 27 | 28 | Application.SetHighDpiMode(HighDpiMode.SystemAware); 29 | Application.EnableVisualStyles(); 30 | Application.SetCompatibleTextRenderingDefault(false); 31 | Application.Run(new TrayApplication(args)); 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /CenterTaskbar/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | // The following GUID is for the ID of the typelib if this project is exposed to COM 4 | [assembly: Guid("939efb11-a324-4c2c-8e16-e6529b3b0ff4")] 5 | -------------------------------------------------------------------------------- /CenterTaskbar/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace CenterTaskbar.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("CenterTaskbar.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). 65 | /// 66 | internal static System.Drawing.Icon TrayIcon { 67 | get { 68 | object obj = ResourceManager.GetObject("TrayIcon", resourceCulture); 69 | return ((System.Drawing.Icon)(obj)); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /CenterTaskbar/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | ..\resources\trayicon.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 123 | 124 | -------------------------------------------------------------------------------- /CenterTaskbar/Resources/TrayIcon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdhiggins/CenterTaskbar/162cf36dba3f4c7b957391fa391654ee31a13202/CenterTaskbar/Resources/TrayIcon.ico -------------------------------------------------------------------------------- /CenterTaskbar/TrayApplication.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Runtime.InteropServices; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using System.Windows.Automation; 9 | using System.Windows.Forms; 10 | using CenterTaskbar.Properties; 11 | using Microsoft.Win32; 12 | 13 | namespace CenterTaskbar 14 | { 15 | public class TrayApplication : ApplicationContext 16 | { 17 | private const string AppName = "CenterTaskbar"; 18 | private const string RunRegkey = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run"; 19 | private const int OneSecond = 1000; 20 | private const int SWP_NOSIZE = 0x0001; 21 | private const int SWP_NOZORDER = 0x0004; 22 | //private const int SWP_SHOWWINDOW = 0x0040; 23 | private const int SWP_ASYNCWINDOWPOS = 0x4000; 24 | private const string MSTaskListWClass = "MSTaskListWClass"; 25 | //private const String ReBarWindow32 = "ReBarWindow32"; 26 | private const string ShellTrayWnd = "Shell_TrayWnd"; 27 | private const string ShellSecondaryTrayWnd = "Shell_SecondaryTrayWnd"; 28 | 29 | private static readonly string ExecutablePath = "\"" + Application.ExecutablePath + "\""; 30 | private static bool _disposed; 31 | private CancellationTokenSource _loopCancellationTokenSource = new(); 32 | 33 | private static readonly AutomationElement Desktop = AutomationElement.RootElement; 34 | private static AutomationEventHandler _uiaEventHandler; 35 | private static AutomationPropertyChangedEventHandler _propChangeHandler; 36 | private static StructureChangedEventHandler _structChangeHandler; 37 | 38 | 39 | private readonly int _activeFramerate = DisplaySettings.CurrentRefreshRate(); 40 | private readonly List _bars = new(); 41 | 42 | private readonly Dictionary _children = new(); 43 | 44 | private readonly Dictionary _lasts = new(); 45 | 46 | private readonly NotifyIcon _trayIcon; 47 | 48 | // private Thread positionThread; 49 | private readonly Dictionary _positionThreads = new(); 50 | 51 | public TrayApplication(IReadOnlyList args) 52 | { 53 | if (args.Count > 0) 54 | try 55 | { 56 | _activeFramerate = int.Parse(args[0]); 57 | Debug.WriteLine("Active refresh rate: " + _activeFramerate); 58 | } 59 | catch (FormatException e) 60 | { 61 | Debug.WriteLine(e.Message); 62 | } 63 | 64 | var header = new ToolStripMenuItem("CenterTaskbar (" + _activeFramerate + ")", null, Exit) 65 | { 66 | Enabled = false 67 | }; 68 | 69 | var startup = new ToolStripMenuItem("Start with Windows", null, ToggleStartup) 70 | { 71 | Checked = IsApplicationInStartup() 72 | }; 73 | 74 | 75 | // Setup Tray Icon 76 | _trayIcon = new NotifyIcon 77 | { 78 | Icon = Resources.TrayIcon, 79 | ContextMenuStrip = new ContextMenuStrip 80 | { 81 | Items = { 82 | header, 83 | new ToolStripMenuItem("Scan for screens", null, Restart), 84 | startup, 85 | new ToolStripMenuItem("E&xit", null, Exit) 86 | } 87 | }, 88 | Visible = true 89 | }; 90 | 91 | Start(); 92 | SystemEvents.DisplaySettingsChanging += SystemEvents_DisplaySettingsChanged; 93 | } 94 | 95 | [DllImport("user32.dll", SetLastError = true)] 96 | private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, int uFlags); 97 | 98 | public void ToggleStartup(object sender, EventArgs e) 99 | { 100 | if (IsApplicationInStartup()) 101 | { 102 | RemoveApplicationFromStartup(); 103 | ((ToolStripMenuItem)sender).Checked = false; 104 | } 105 | else 106 | { 107 | AddApplicationToStartup(); 108 | ((ToolStripMenuItem)sender).Checked = true; 109 | } 110 | } 111 | 112 | public bool IsApplicationInStartup() 113 | { 114 | using var key = Registry.CurrentUser.OpenSubKey(RunRegkey, true); 115 | var value = key?.GetValue(AppName); 116 | return value is string startValue && startValue.StartsWith(ExecutablePath); 117 | } 118 | 119 | public void AddApplicationToStartup() 120 | { 121 | using var key = Registry.CurrentUser.OpenSubKey(RunRegkey, true); 122 | key?.SetValue(AppName, ExecutablePath); 123 | } 124 | 125 | public void RemoveApplicationFromStartup() 126 | { 127 | using var key = Registry.CurrentUser.OpenSubKey(RunRegkey, true); 128 | key?.DeleteValue(AppName, false); 129 | } 130 | 131 | private void Exit(object sender, EventArgs e) 132 | { 133 | SystemEvents.DisplaySettingsChanging -= SystemEvents_DisplaySettingsChanged; 134 | Application.ExitThread(); 135 | } 136 | 137 | private void CancelPositionThread() 138 | { 139 | try 140 | { 141 | _loopCancellationTokenSource.Cancel(); 142 | Parallel.ForEach(_positionThreads.Values.ToList(), theTask => 143 | { 144 | try 145 | { 146 | // Give the thread time to exit gracefully. 147 | if (theTask.Wait(OneSecond * 3)) return; 148 | } 149 | catch (OperationCanceledException e) 150 | { 151 | Console.WriteLine($"{nameof(OperationCanceledException)} thrown with message: {e.Message}"); 152 | } 153 | finally 154 | { 155 | theTask.Dispose(); 156 | } 157 | }); 158 | } 159 | catch (OperationCanceledException e) 160 | { 161 | Console.WriteLine($"{nameof(OperationCanceledException)} thrown with message: {e.Message}"); 162 | } 163 | finally 164 | { 165 | _loopCancellationTokenSource = new CancellationTokenSource(); 166 | } 167 | } 168 | 169 | private async void Restart(object sender, EventArgs e) 170 | { 171 | CancelPositionThread(); 172 | try 173 | { 174 | Start(); 175 | } 176 | catch (NullReferenceException) 177 | { 178 | await Task.Delay(100); 179 | Start(); 180 | } 181 | 182 | } 183 | 184 | private void ResetAll() 185 | { 186 | CancelPositionThread(); 187 | Parallel.ForEach(_bars.ToList(), Reset); 188 | } 189 | 190 | private static void Reset(AutomationElement trayWnd) 191 | { 192 | Debug.WriteLine("Begin Reset Calculation"); 193 | 194 | var taskList = trayWnd.FindFirst(TreeScope.Descendants, 195 | new PropertyCondition(AutomationElement.ClassNameProperty, MSTaskListWClass)); 196 | if (taskList == null) 197 | { 198 | Debug.WriteLine("Null values found, aborting reset"); 199 | return; 200 | } 201 | 202 | var taskListContainer = TreeWalker.ControlViewWalker.GetParent(taskList); 203 | if (taskListContainer == null) 204 | { 205 | Debug.WriteLine("Null values found, aborting reset"); 206 | return; 207 | } 208 | 209 | var taskListPtr = (IntPtr)taskList.Current.NativeWindowHandle; 210 | 211 | SetWindowPos(taskListPtr, IntPtr.Zero, 0, 0, 0, 0, SWP_NOZORDER | SWP_NOSIZE | SWP_ASYNCWINDOWPOS); 212 | } 213 | 214 | private void Start() 215 | { 216 | _propChangeHandler = OnUIAutomationEvent; 217 | _structChangeHandler = OnUIAutomationEvent; 218 | _uiaEventHandler = OnUIAutomationEvent; 219 | 220 | var condition = new OrCondition(new PropertyCondition(AutomationElement.ClassNameProperty, ShellTrayWnd), 221 | new PropertyCondition(AutomationElement.ClassNameProperty, ShellSecondaryTrayWnd)); 222 | var cacheRequest = new CacheRequest(); 223 | cacheRequest.Add(AutomationElement.NameProperty); 224 | cacheRequest.Add(AutomationElement.ClassNameProperty); 225 | cacheRequest.Add(AutomationElement.BoundingRectangleProperty); 226 | 227 | _bars.Clear(); 228 | _children.Clear(); 229 | _lasts.Clear(); 230 | 231 | using (cacheRequest.Activate()) 232 | { 233 | var lists = Desktop.FindAll(TreeScope.Children, condition); 234 | if (lists == null) 235 | { 236 | Debug.WriteLine("Null values found, aborting"); 237 | return; 238 | } 239 | 240 | Debug.WriteLine(lists.Count + " bar(s) detected"); 241 | _lasts.Clear(); 242 | 243 | Condition taskListProperty = new PropertyCondition(AutomationElement.ClassNameProperty, MSTaskListWClass); 244 | 245 | Parallel.ForEach(lists.OfType(), trayWnd => 246 | { 247 | var taskList = trayWnd.FindFirst(TreeScope.Descendants, taskListProperty); 248 | 249 | if (taskList == null) 250 | { 251 | Debug.WriteLine("Null values found, aborting"); 252 | } 253 | else 254 | { 255 | Automation.AddAutomationPropertyChangedEventHandler(taskList, TreeScope.Element | TreeScope.Children, _propChangeHandler, 256 | AutomationElement.BoundingRectangleProperty); 257 | Automation.AddStructureChangedEventHandler(trayWnd, TreeScope.Element | TreeScope.Descendants | TreeScope.Children, _structChangeHandler); 258 | _bars.Add(trayWnd); 259 | _children.Add(trayWnd, taskList); 260 | 261 | _positionThreads[trayWnd] = Task.Run(() => LoopForPosition(trayWnd), _loopCancellationTokenSource.Token); 262 | } 263 | }); 264 | } 265 | 266 | Automation.AddAutomationEventHandler(WindowPattern.WindowOpenedEvent, Desktop, TreeScope.Subtree, _uiaEventHandler); 267 | Automation.AddAutomationEventHandler(WindowPattern.WindowClosedEvent, Desktop, TreeScope.Subtree, _uiaEventHandler); 268 | } 269 | 270 | private async void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e) 271 | { 272 | await Task.Delay(3000); 273 | Restart(sender, e); 274 | } 275 | 276 | private void OnUIAutomationEvent(object src, AutomationEventArgs e) 277 | { 278 | if (e is AutomationPropertyChangedEventArgs) 279 | { 280 | Debug.Print("Event occured: {0} {1}", e.EventId.ProgrammaticName, (e as AutomationPropertyChangedEventArgs).Property.ProgrammaticName); 281 | } else 282 | { 283 | Debug.Print("Event occured: {0}", e.EventId.ProgrammaticName); 284 | } 285 | Parallel.ForEach(_bars.ToList(), trayWnd => 286 | { 287 | if (_positionThreads[trayWnd].IsCompleted) 288 | { 289 | Debug.WriteLine("Starting new thead"); 290 | _positionThreads[trayWnd] = Task.Run(() => LoopForPosition(trayWnd), _loopCancellationTokenSource.Token); 291 | } 292 | else 293 | { 294 | Debug.WriteLine("Thread already exists"); 295 | } 296 | }); 297 | } 298 | 299 | private void LoopForPosition(object trayWndObj) 300 | { 301 | var trayWnd = (AutomationElement)trayWndObj; 302 | var numberOfLoops = _activeFramerate / 10; 303 | var keepGoing = 0; 304 | while (keepGoing < numberOfLoops) 305 | { 306 | if (!PositionLoop(trayWnd)) keepGoing += 1; 307 | if (_loopCancellationTokenSource.IsCancellationRequested) break; 308 | Task.Delay(OneSecond / _activeFramerate).Wait(); 309 | } 310 | 311 | Debug.WriteLine("LoopForPosition Thread ended."); 312 | } 313 | 314 | private bool PositionLoop(AutomationElement trayWnd) 315 | { 316 | Debug.WriteLine("Begin Reposition Calculation"); 317 | 318 | var taskList = _children[trayWnd]; 319 | var last = TreeWalker.ControlViewWalker.GetLastChild(taskList); 320 | if (last == null) 321 | { 322 | Debug.WriteLine("Null values found for items, aborting"); 323 | return true; 324 | } 325 | 326 | var trayBounds = trayWnd.Cached.BoundingRectangle; 327 | var horizontal = trayBounds.Width > trayBounds.Height; 328 | 329 | // Use the left/top bounds because there is an empty element as the last child with a nonzero width 330 | var lastChildPos = horizontal ? last.Current.BoundingRectangle.Left : last.Current.BoundingRectangle.Top; 331 | Debug.WriteLine("Last child position: " + lastChildPos); 332 | 333 | if (_lasts.ContainsKey(trayWnd) && lastChildPos == _lasts[trayWnd]) 334 | { 335 | Debug.WriteLine("Size/location unchanged, sleeping"); 336 | return false; 337 | } 338 | 339 | Debug.WriteLine("Size/location changed, recalculating center"); 340 | _lasts[trayWnd] = lastChildPos; 341 | 342 | var first = TreeWalker.ControlViewWalker.GetFirstChild(taskList); 343 | if (first == null) 344 | { 345 | Debug.WriteLine("Null values found for first child item, aborting"); 346 | return true; 347 | } 348 | 349 | var iconSizeSetting = (int)Registry.GetValue(@"HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced", "TaskbarSmallIcons", 0); 350 | var iconSizeHorizontal = (iconSizeSetting == 0) ? 40 : 30; 351 | var iconSizeVertical = (iconSizeSetting == 0) ? 47 : 31; 352 | 353 | var scale = horizontal 354 | ? first.Current.BoundingRectangle.Height / iconSizeHorizontal 355 | : first.Current.BoundingRectangle.Height / iconSizeVertical; 356 | Debug.WriteLine("UI Scale: " + scale); 357 | var size = (lastChildPos - (horizontal 358 | ? first.Current.BoundingRectangle.Left 359 | : first.Current.BoundingRectangle.Top) 360 | ) / scale; 361 | if (size < 0) 362 | { 363 | Debug.WriteLine("Size calculation failed"); 364 | return true; 365 | } 366 | 367 | var taskListContainer = TreeWalker.ControlViewWalker.GetParent(taskList); 368 | if (taskListContainer == null) 369 | { 370 | Debug.WriteLine("Null values found for parent, aborting"); 371 | return true; 372 | } 373 | 374 | var taskListBounds = taskList.Current.BoundingRectangle; 375 | 376 | var barSize = horizontal ? trayWnd.Cached.BoundingRectangle.Width : trayWnd.Cached.BoundingRectangle.Height; 377 | var targetPos = Math.Round((barSize - size) / 2) + (horizontal ? trayBounds.X : trayBounds.Y); 378 | 379 | Debug.Write("Bar size: "); 380 | Debug.WriteLine(barSize); 381 | Debug.Write("Total icon size: "); 382 | Debug.WriteLine(size); 383 | Debug.Write("Target abs " + (horizontal ? "X" : "Y") + " position: "); 384 | Debug.WriteLine(targetPos); 385 | 386 | var delta = Math.Abs(targetPos - (horizontal ? taskListBounds.X : taskListBounds.Y)); 387 | // Previous bounds check 388 | if (delta <= 1) 389 | { 390 | // Already positioned within margin of error, avoid the unneeded MoveWindow call 391 | Debug.WriteLine("Already positioned, ending to avoid the unneeded MoveWindow call (Delta: " + delta + ")"); 392 | return false; 393 | } 394 | 395 | int rightBounds; 396 | int leftBounds; 397 | try 398 | { 399 | rightBounds = SideBoundary(false, horizontal, taskList, scale, trayBounds); 400 | leftBounds = SideBoundary(true, horizontal, taskList, scale, trayBounds); 401 | } 402 | catch (NullReferenceException) 403 | { 404 | Reset(trayWnd); 405 | return true; 406 | } 407 | 408 | // Right bounds check 409 | if (targetPos + size > rightBounds) 410 | { 411 | // Shift off center when the bar is too big 412 | var extra = targetPos + size - rightBounds; 413 | Debug.WriteLine("Shifting off center, too big and hitting right/bottom boundary (" + (targetPos + size) + " > " + rightBounds + ") // " + extra); 414 | targetPos -= extra; 415 | } 416 | 417 | // Left bounds check 418 | if (targetPos <= leftBounds) 419 | { 420 | // Prevent X position ending up beyond the normal left aligned position 421 | Debug.WriteLine("Target is more left than left/top aligned default, left/top aligning (" + targetPos + " <= " + leftBounds + ")"); 422 | Reset(trayWnd); 423 | return true; 424 | } 425 | 426 | var taskListPtr = (IntPtr)taskList.Current.NativeWindowHandle; 427 | 428 | if (horizontal) 429 | { 430 | SetWindowPos(taskListPtr, IntPtr.Zero, RelativePos(targetPos, horizontal, taskList, scale, trayBounds), 0, 0, 0, 431 | SWP_NOZORDER | SWP_NOSIZE | SWP_ASYNCWINDOWPOS); 432 | Debug.Write("Final X Position: "); 433 | Debug.WriteLine(((first.Current.BoundingRectangle.Left - trayBounds.Left) / scale) + trayBounds.Left); 434 | Debug.Write(Math.Round(((first.Current.BoundingRectangle.Left - trayBounds.Left) / scale) + trayBounds.Left) == Math.Round(targetPos) ? "Move hit target" : "Move missed target"); 435 | Debug.WriteLine(" (diff: " + Math.Abs((((first.Current.BoundingRectangle.Left - trayBounds.Left) / scale) + trayBounds.Left) - targetPos) + ")"); 436 | } 437 | else 438 | { 439 | SetWindowPos(taskListPtr, IntPtr.Zero, 0, RelativePos(targetPos, horizontal, taskList, scale, trayBounds), 0, 0, 440 | SWP_NOZORDER | SWP_NOSIZE | SWP_ASYNCWINDOWPOS); 441 | Debug.Write("Final Y Position: "); 442 | Debug.WriteLine(((first.Current.BoundingRectangle.Top - trayBounds.Top) / scale) + trayBounds.Top); 443 | Debug.Write(Math.Round(((first.Current.BoundingRectangle.Top - trayBounds.Top) / scale) + trayBounds.Top) == Math.Round(targetPos) ? "Move hit target" : "Move missed target"); 444 | Debug.WriteLine(" (diff: " + Math.Abs((((first.Current.BoundingRectangle.Top - trayBounds.Top) / scale) + trayBounds.Top) - targetPos) + ")"); 445 | } 446 | 447 | _lasts[trayWnd] = horizontal ? last.Current.BoundingRectangle.Left : last.Current.BoundingRectangle.Top; 448 | 449 | return true; 450 | } 451 | 452 | private static int RelativePos(double x, bool horizontal, AutomationElement element, double scale, System.Windows.Rect trayBounds) 453 | { 454 | var adjustment = SideBoundary(true, horizontal, element, scale, trayBounds); 455 | 456 | var newPos = x - adjustment; 457 | 458 | if (newPos < 0) 459 | { 460 | Debug.WriteLine("Relative position < 0, adjusting to 0 (Previous: " + newPos + ")"); 461 | newPos = 0; 462 | } 463 | 464 | return (int)newPos; 465 | } 466 | 467 | private static int SideBoundary(bool left, bool horizontal, AutomationElement element, double scale, System.Windows.Rect trayBounds) 468 | { 469 | double adjustment = 0; 470 | Debug.WriteLine("Boundary calc for " + element.Current.ClassName); 471 | var prevSibling = TreeWalker.RawViewWalker.GetPreviousSibling(element); 472 | var nextSibling = TreeWalker.RawViewWalker.GetNextSibling(element); 473 | var first = TreeWalker.RawViewWalker.GetFirstChild(element); 474 | var parent = TreeWalker.RawViewWalker.GetParent(element); 475 | 476 | var padding = horizontal ? (trayBounds.Left - element.Current.BoundingRectangle.Left) - ((trayBounds.Left - first.Current.BoundingRectangle.Left) / scale) : (trayBounds.Top - element.Current.BoundingRectangle.Top) - ((trayBounds.Top - first.Current.BoundingRectangle.Top) / scale); 477 | 478 | Debug.Write(horizontal ? "Horizontal Padding: " : "Vertical Padding: "); 479 | Debug.WriteLine(Math.Round(padding)); 480 | if (padding < 0) 481 | { 482 | Debug.WriteLine("Padding should not be less than 0, setting to 0"); 483 | padding = 0; 484 | } 485 | 486 | if (left && prevSibling != null && !prevSibling.Current.BoundingRectangle.IsEmpty) 487 | { 488 | Debug.WriteLine("Left sibling calc " + prevSibling.Current.ClassName); 489 | adjustment = horizontal 490 | ? prevSibling.Current.BoundingRectangle.Right 491 | : prevSibling.Current.BoundingRectangle.Bottom; 492 | } 493 | else if (!left && nextSibling != null && !nextSibling.Current.BoundingRectangle.IsEmpty) 494 | { 495 | Debug.WriteLine("Right sibling calc " + nextSibling.Current.ClassName); 496 | adjustment = horizontal 497 | ? nextSibling.Current.BoundingRectangle.Left 498 | : nextSibling.Current.BoundingRectangle.Top; 499 | } 500 | else if (parent != null) 501 | { 502 | Debug.WriteLine("Parent calc " + parent.Current.ClassName); 503 | if (horizontal) 504 | adjustment = left ? parent.Current.BoundingRectangle.Left + padding : parent.Current.BoundingRectangle.Right; 505 | else 506 | adjustment = left ? parent.Current.BoundingRectangle.Top + padding : parent.Current.BoundingRectangle.Bottom; 507 | } 508 | 509 | if (horizontal) 510 | Debug.WriteLine((left ? "Left" : "Right") + " side boundary calculated at " + adjustment); 511 | else 512 | Debug.WriteLine((left ? "Top" : "Bottom") + " side boundary calculated at " + adjustment); 513 | 514 | return (int)adjustment; 515 | } 516 | 517 | // Protected implementation of Dispose pattern. 518 | protected override void Dispose(bool disposing) 519 | { 520 | if (_disposed) 521 | return; 522 | 523 | if (disposing) 524 | { 525 | // Stop listening for new events 526 | Automation.RemoveAllEventHandlers(); 527 | 528 | // Put icons back 529 | ResetAll(); 530 | 531 | // Hide tray icon, otherwise it will remain shown until user mouses over it 532 | _trayIcon.Visible = false; 533 | _trayIcon.Dispose(); 534 | } 535 | 536 | _disposed = true; 537 | } 538 | } 539 | } 540 | -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2018 Michael Higgins. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CenterTaskbar 2 | 3 | ![Gif](https://user-images.githubusercontent.com/3608298/49901443-36234800-fe2f-11e8-89dd-9ab609a34fba.gif) 4 | 5 | ---- 6 | ## Archived 7 | * Windows 11 will not be supported. Underlying changes to the taskbar break the technique this program uses to center the icons so this solution no longer functions and since Windows 11 implements native centering there will be no updates to support Windows 11. I've swithced to using StartAllBack which offers way more features and cleaner animations. Archiving this project for record purposes and it will remain available to use 8 | 9 | ## Features 10 | * Dynamic - works regardless of number of icons, DPI scaling grouping, size. All padding is calculated 11 | * Animated - resizes along with default windows animations 12 | * Performant - sleeps when no resizing taking place to 0% CPU usage 13 | * Multimonitor suppport 14 | * Vertical orientation support 15 | * Multiple DPI support 16 | 17 | ## Usage 18 | Run the program and let it run in the background. It uses Windows UIAutomation to monitor for position changes and calculate a new position to center the taskbar items. 19 | 20 | ## Command Line Args 21 | First command line argument sets the refresh rate in hertz during active icon changes. Default `60`. Recommended to sync to your monitor refresh rate or higher. When no changes are being made program goes to sleep and awaits for events triggered by UIAutomation to restart the repositioning thread allowing it to drop to 0% CPU usage. 22 | 23 | Specifically it will monitor for: 24 | * `WindowOpenedEvent` 25 | * `WindowClosedEvent` 26 | * `AutomationPropertyChangedEvent: BoundingRectangleProperty` 27 | * `StructureChangedEvent` 28 | --------------------------------------------------------------------------------