├── .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 | 
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 |
--------------------------------------------------------------------------------