diff --git a/App.xaml b/App.xaml
new file mode 100644
index 0000000..919c746
--- /dev/null
+++ b/App.xaml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/App.xaml.cs b/App.xaml.cs
new file mode 100644
index 0000000..ed4a219
--- /dev/null
+++ b/App.xaml.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Windows;
+using BackgroundBuilder.Repositories;
+using BackgroundBuilder.Services;
+using BackgroundBuilder.ViewModels;
+using BackgroundBuilder.Views;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace BackgroundBuilder
+{
+ public partial class App : Application
+ {
+ private readonly IHost _host;
+
+ public App()
+ {
+ _host = Host.CreateDefaultBuilder()
+ .ConfigureAppConfiguration(cfg =>
+ cfg.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true))
+ .ConfigureServices((_, services) =>
+ {
+ // Database & repository
+ services.AddSingleton();
+ services.AddScoped();
+
+ // New image service
+ services.AddSingleton();
+
+ // New taskbar service
+ services.AddSingleton();
+
+ // VM & View
+ services.AddTransient();
+ services.AddTransient();
+ })
+ .Build();
+ }
+
+ private async void OnStartup(object sender, StartupEventArgs e)
+ {
+ await _host.StartAsync();
+ var window = _host.Services.GetRequiredService();
+ window.Show();
+ }
+
+ protected override async void OnExit(ExitEventArgs e)
+ {
+ using (_host) { await _host.StopAsync(); }
+ base.OnExit(e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/AssemblyInfo.cs b/AssemblyInfo.cs
new file mode 100644
index 0000000..b0ec827
--- /dev/null
+++ b/AssemblyInfo.cs
@@ -0,0 +1,10 @@
+using System.Windows;
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
diff --git a/BackgroundBuilder.csproj b/BackgroundBuilder.csproj
new file mode 100644
index 0000000..f70ee4d
--- /dev/null
+++ b/BackgroundBuilder.csproj
@@ -0,0 +1,29 @@
+
+
+
+ WinExe
+ net9.0-windows7.0
+ true
+ BackgroundBuilder
+ BackgroundBuilder
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/BackgroundBuilder.sln b/BackgroundBuilder.sln
new file mode 100644
index 0000000..d8ea512
--- /dev/null
+++ b/BackgroundBuilder.sln
@@ -0,0 +1,22 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.12.35527.113 d17.12
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BackgroundBuilder", "BackgroundBuilder.csproj", "{4AC89EE0-00B5-47D2-924B-FD65DD09E67B}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {4AC89EE0-00B5-47D2-924B-FD65DD09E67B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4AC89EE0-00B5-47D2-924B-FD65DD09E67B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4AC89EE0-00B5-47D2-924B-FD65DD09E67B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4AC89EE0-00B5-47D2-924B-FD65DD09E67B}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/Converters/DateNoDotConverter.cs b/Converters/DateNoDotConverter.cs
new file mode 100644
index 0000000..34b458f
--- /dev/null
+++ b/Converters/DateNoDotConverter.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace BackgroundBuilder.Converters
+{
+ public class DateNoDotConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is DateTime dt)
+ {
+ // get "MMM" then TrimEnd('.'):
+ string month = culture
+ .DateTimeFormat
+ .GetAbbreviatedMonthName(dt.Month)
+ .TrimEnd('.');
+ return $"{dt:dd}/{month}";
+ }
+ return value;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ => throw new NotImplementedException();
+ }
+}
diff --git a/Models/Contato.cs b/Models/Contato.cs
new file mode 100644
index 0000000..56a857c
--- /dev/null
+++ b/Models/Contato.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace BackgroundBuilder.Models
+{
+ public class Contato
+ {
+ public string Ramal { get; set; } = string.Empty;
+ public string? Nome { get; set; } = string.Empty;
+ public string? Email { get; set; }
+ public string? Area { get; set; }
+ public DateTime? Aniversario { get; set; }
+ public bool IsComando { get; set; }
+ }
+}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b3c668e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,46 @@
+# BackgroundBuilder
+
+An MVVM WPF application (.NET 6) providing an Excel-like editor for the `contatos` table in PostgreSQL.
+
+## Prerequisites
+
+- .NET 6 SDK
+- PostgreSQL database with table:
+
+ ```sql
+ CREATE TABLE public.contatos (
+ ramal text PRIMARY KEY NOT NULL,
+ nome text NOT NULL,
+ email text,
+ area text,
+ aniversario date,
+ "isComando" boolean NOT NULL
+ );
+ ```
+## Setup
+1. Edit appsettings.json, set your ConnectionStrings:ContatosDb.
+2. In a terminal:
+ ```bash
+ dotnet restore
+ dotnet build
+ dotnet run --project BackgroundBuilder.csproj
+ ```
+3. The main window will appear; on load it fetches and displays all contatos.
+
+## Architecture
+- MVVM with MVVM with ObservableObject, RelayCommand.
+- DI via Microsoft.Extensions.Hosting.
+- Repositories (PostgresContatoRepository) handle all DB I/O with Dapper.
+- Services (DatabaseService) manage the Npgsql connection.
+- ViewModels free of data-access logic: only orchestration.
+---
+
+### New Background & Export Features
+
+- **Select Background…**
+ Opens a file picker—choose any image (PNG, JPG, BMP). That image becomes your canvas.
+
+- **Create Image…**
+ Saves the current DataGrid overlaid on the background as a single PNG.
+ Uses WPF’s `RenderTargetBitmap` and `PngBitmapEncoder` under the hood.
+---
\ No newline at end of file
diff --git a/Repositories/IContatoRepository.cs b/Repositories/IContatoRepository.cs
new file mode 100644
index 0000000..8d07791
--- /dev/null
+++ b/Repositories/IContatoRepository.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using BackgroundBuilder.Models;
+
+namespace BackgroundBuilder.Repositories
+{
+ public interface IContatoRepository
+ {
+ Task> GetAllAsync();
+ Task InsertUpdateAsync(Contato c);
+ Task DeleteAsync(string ramal);
+ }
+}
diff --git a/Repositories/PostgresContatoRepository.cs b/Repositories/PostgresContatoRepository.cs
new file mode 100644
index 0000000..a448efc
--- /dev/null
+++ b/Repositories/PostgresContatoRepository.cs
@@ -0,0 +1,71 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Dapper;
+using Npgsql;
+using BackgroundBuilder.Models;
+using BackgroundBuilder.Services;
+
+namespace BackgroundBuilder.Repositories
+{
+ public class PostgresContatoRepository(DatabaseService db) : IContatoRepository
+ {
+ private readonly DatabaseService _db = db;
+
+ public async Task> GetAllAsync()
+ {
+ using var conn = _db.CreateConnection();
+ const string sql = @"
+ SELECT
+ ramal AS Ramal,
+ nome AS Nome,
+ email AS Email,
+ area AS Area,
+ aniversario AS Aniversario,
+ ""isComando"" AS IsComando
+ FROM contatos;";
+ return await conn.QueryAsync(sql);
+ }
+
+ public async Task AddAsync(Contato c)
+ {
+ using var conn = _db.CreateConnection();
+ const string sql = @"
+ INSERT INTO contatos
+ (ramal, nome, email, area, aniversario, ""isComando"")
+ VALUES
+ (@Ramal, @Nome, @Email, @Area, @Aniversario, @IsComando);";
+ await conn.ExecuteAsync(sql, c);
+ }
+
+ public async Task DeleteAsync(string ramal)
+ {
+ using var conn = _db.CreateConnection();
+ const string sql = "DELETE FROM contatos WHERE ramal=@Ramal;";
+ await conn.ExecuteAsync(sql, new { Ramal = ramal });
+ }
+
+ public async Task InsertUpdateAsync(Contato c)
+ {
+ using var conn = _db.CreateConnection();
+ string Ramal = $"'{c.Ramal}'";
+ string Nome = c.Nome is null ? "null" : $"'{c.Nome}'";
+ string Email = c.Email is null ? "null" : $"'{c.Email}'";
+ string Area = c.Area == "" ? "null" : $"'{c.Area}'";
+ string Aniversario = c.Aniversario is null ? "null" : $"'{c.Aniversario:yyyy-MM-dd}'";
+ string IsComando = c.IsComando ? "true" : "false";
+
+ string sql = @$"
+ INSERT INTO contatos
+ (ramal, nome, email, area, aniversario, ""isComando"")
+ VALUES
+ ({Ramal}, {Nome}, {Email}, {Area}, {Aniversario:YYYY-MM-DD}, {IsComando})
+ ON CONFLICT (ramal) DO UPDATE SET
+ nome = EXCLUDED.nome,
+ email = EXCLUDED.email,
+ area = EXCLUDED.area,
+ aniversario = EXCLUDED.aniversario,
+ ""isComando"" = EXCLUDED.""isComando"";";
+ await conn.ExecuteAsync(sql);
+ }
+ }
+}
diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs
new file mode 100644
index 0000000..20ad871
--- /dev/null
+++ b/Services/DatabaseService.cs
@@ -0,0 +1,19 @@
+using System;
+using Microsoft.Extensions.Configuration;
+using Npgsql;
+
+namespace BackgroundBuilder.Services
+{
+ public class DatabaseService(IConfiguration config)
+ {
+ private readonly string _connString = config.GetConnectionString("ContatosDb")
+ ?? throw new InvalidOperationException("Missing connection string 'ContatosDb'.");
+
+ public NpgsqlConnection CreateConnection()
+ {
+ var conn = new NpgsqlConnection(_connString);
+ conn.Open();
+ return conn;
+ }
+ }
+}
diff --git a/Services/IImageService.cs b/Services/IImageService.cs
new file mode 100644
index 0000000..56f9144
--- /dev/null
+++ b/Services/IImageService.cs
@@ -0,0 +1,26 @@
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Media.Imaging;
+
+namespace BackgroundBuilder.Services
+{
+ public interface IImageService
+ {
+ ///
+ /// Loads an image from disk.
+ ///
+ Task LoadAsync(string path);
+
+ ///
+ /// Renders the background + overlay and writes two PNGs:
+ /// • primaryPath: composite of background+overlay
+ /// • overlayPath (optional): overlay alone
+ /// Returns the actual paths written.
+ ///
+ Task<(string primaryPath, string? overlayPath)> SaveAsync(
+ FrameworkElement overlay,
+ BitmapImage background,
+ string primaryPath,
+ string? overlayPath = null);
+ }
+}
diff --git a/Services/ITaskbarService.cs b/Services/ITaskbarService.cs
new file mode 100644
index 0000000..c3305a7
--- /dev/null
+++ b/Services/ITaskbarService.cs
@@ -0,0 +1,13 @@
+namespace BackgroundBuilder.Services
+{
+ ///
+ /// Defines methods for querying the Windows taskbar size and orientation.
+ ///
+ public interface ITaskbarService
+ {
+ /// Gets the current thickness (height if horizontal, width if vertical) of the taskbar in pixels.
+ int GetTaskbarSize();
+ /// Returns true when the taskbar is horizontal, false when vertical.
+ bool IsHorizontal();
+ }
+}
\ No newline at end of file
diff --git a/Services/ImageService.cs b/Services/ImageService.cs
new file mode 100644
index 0000000..bae1c37
--- /dev/null
+++ b/Services/ImageService.cs
@@ -0,0 +1,115 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+
+namespace BackgroundBuilder.Services
+{
+ public class ImageService : IImageService
+ {
+
+ // no longer injected — use a private constant
+ private static readonly Thickness OverlayOffset = new(10, 58, 10, 58);
+
+ public async Task LoadAsync(string path)
+ {
+ // Load image from disk into BitmapImage
+ var bmp = new BitmapImage();
+ using var stream = File.OpenRead(path);
+ bmp.BeginInit();
+ bmp.UriSource = new Uri(path, UriKind.Absolute);
+ bmp.CacheOption = BitmapCacheOption.OnLoad;
+ bmp.EndInit();
+ // no real async work—return completed task
+ return await Task.FromResult(bmp);
+ }
+
+ public async Task<(string primaryPath, string? overlayPath)> SaveAsync(
+ FrameworkElement overlay,
+ BitmapImage background,
+ string primaryPath,
+ string? overlayPath = null)
+ {
+ var compositeBmp = RenderComposite(overlay, background, OverlayOffset);
+ SaveBitmap(compositeBmp, primaryPath);
+
+ string? savedOverlayPath = null;
+ if (!string.IsNullOrWhiteSpace(overlayPath))
+ {
+ var overlayBmp = RenderComposite(overlay, null, new Thickness(0));
+ SaveBitmap(overlayBmp, overlayPath!);
+ savedOverlayPath = overlayPath;
+ }
+
+ return await Task.FromResult((primaryPath, savedOverlayPath));
+ }
+
+ ///
+ /// Renders a plus the
+ /// at bottom‐right offset by . If background is null,
+ /// only the mainGrid is rendered at (0,0).
+ ///
+ private static RenderTargetBitmap RenderComposite(
+ FrameworkElement mainGrid,
+ BitmapImage? background,
+ Thickness offset)
+ {
+ // Determine canvas size
+ int width = background?.PixelWidth ?? (int)mainGrid.ActualWidth;
+ int height = background?.PixelHeight ?? (int)mainGrid.ActualHeight;
+
+ var dv = new DrawingVisual();
+ using (var ctx = dv.RenderOpen())
+ {
+ // Draw background if provided
+ if (background != null)
+ {
+ ctx.DrawImage(
+ background,
+ new Rect(0, 0, width, height));
+ }
+
+ // Measure & arrange the mainGrid so ActualWidth/Height are valid
+ mainGrid.Measure(new Size(width, height));
+ mainGrid.Arrange(new Rect(0, 0, mainGrid.DesiredSize.Width, mainGrid.DesiredSize.Height));
+
+ double ow = mainGrid.ActualWidth > 0 ? mainGrid.ActualWidth : mainGrid.DesiredSize.Width;
+ double oh = mainGrid.ActualHeight > 0 ? mainGrid.ActualHeight : mainGrid.DesiredSize.Height;
+
+ double x = width - ow - offset.Left;
+ double y = height - oh - offset.Right;
+
+ // Draw mainGrid at either origin or bottom-right with offset
+ var brush = new VisualBrush(mainGrid);
+
+ ctx.DrawRectangle(brush, null, new Rect(x, y, ow, oh));
+ }
+
+ var rtb = new RenderTargetBitmap(
+ width,
+ height,
+ 96,
+ 96,
+ PixelFormats.Pbgra32);
+
+ rtb.Render(dv);
+ return rtb;
+ }
+
+ ///
+ /// Encodes the given bitmap as PNG and writes it to disk.
+ ///
+ private static void SaveBitmap(RenderTargetBitmap bitmap, string path)
+ {
+ // Ensure directory exists
+ Directory.CreateDirectory(Path.GetDirectoryName(path)!);
+
+ using var fs = new FileStream(path, FileMode.Create, FileAccess.Write);
+ var encoder = new PngBitmapEncoder();
+ encoder.Frames.Add(BitmapFrame.Create(bitmap));
+ encoder.Save(fs);
+ }
+ }
+}
diff --git a/Services/TaskbarService.cs b/Services/TaskbarService.cs
new file mode 100644
index 0000000..b6bff08
--- /dev/null
+++ b/Services/TaskbarService.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace BackgroundBuilder.Services
+{
+ ///
+ /// Implementation of using P/Invoke (SHAppBarMessage).
+ ///
+ public class TaskbarService : ITaskbarService
+ {
+ #region WinAPI
+ [DllImport("shell32.dll")]
+ private static extern IntPtr SHAppBarMessage(uint msg, ref APPBARDATA data);
+ [StructLayout(LayoutKind.Sequential)]
+ private struct APPBARDATA
+ {
+ public uint cbSize;
+ public IntPtr hWnd;
+ public uint uCallbackMessage;
+ public uint uEdge;
+ public RECT rc;
+ public IntPtr lParam;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct RECT { public int left, top, right, bottom; }
+
+ private const uint ABM_GETTASKBARPOS = 5;
+ #endregion
+
+ public int GetTaskbarSize()
+ {
+ var data = new APPBARDATA { cbSize = (uint)Marshal.SizeOf() };
+ if (SHAppBarMessage(ABM_GETTASKBARPOS, ref data) == IntPtr.Zero)
+ throw new InvalidOperationException("Failed to retrieve taskbar position.");
+
+ int thickness = IsHorizontal()
+ ? Math.Abs(data.rc.bottom - data.rc.top)
+ : Math.Abs(data.rc.right - data.rc.left);
+
+ return thickness;
+ }
+
+ public bool IsHorizontal()
+ {
+ var data = new APPBARDATA { cbSize = (uint)Marshal.SizeOf() };
+ SHAppBarMessage(ABM_GETTASKBARPOS, ref data);
+ int height = Math.Abs(data.rc.bottom - data.rc.top);
+ int width = Math.Abs(data.rc.right - data.rc.left);
+
+ return width > height;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Utils/ObservableObject.cs b/Utils/ObservableObject.cs
new file mode 100644
index 0000000..f391471
--- /dev/null
+++ b/Utils/ObservableObject.cs
@@ -0,0 +1,12 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace BackgroundBuilder.Utils
+{
+ public abstract class ObservableObject : INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler? PropertyChanged;
+ protected void OnPropertyChanged([CallerMemberName] string? propName = null)
+ => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
+ }
+}
diff --git a/Utils/RelayCommand.cs b/Utils/RelayCommand.cs
new file mode 100644
index 0000000..309b4c8
--- /dev/null
+++ b/Utils/RelayCommand.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Windows.Input;
+
+namespace BackgroundBuilder.Utils
+{
+ public class RelayCommand(Action