39 KiB
Raw Permalink Blame History

Resume

DateNoDotConverter.cs:

//BackgroundBuilder\Converters\DateNoDotConverter.cs
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();
    }
}

Contato.cs:

//BackgroundBuilder\Models\Contato.cs
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; }
    }
}

IContatoRepository.cs:

//BackgroundBuilder\Repositories\IContatoRepository.cs
using System.Collections.Generic;
using System.Threading.Tasks;
using BackgroundBuilder.Models;

namespace BackgroundBuilder.Repositories
{
    public interface IContatoRepository
    {
        Task<IEnumerable<Contato>> GetAllAsync();
        Task InsertUpdateAsync(Contato c);
        Task DeleteAsync(string ramal);
    }
}

PostgresContatoRepository.cs:

//BackgroundBuilder\Repositories\PostgresContatoRepository.cs
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<IEnumerable<Contato>> 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<Contato>(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);
        }
    }
}

DatabaseService.cs:

//BackgroundBuilder\Services\DatabaseService.cs
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;
        }
    }
}

IImageService.cs:

//BackgroundBuilder\Services\IImageService.cs
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Imaging;

namespace BackgroundBuilder.Services
{
    public interface IImageService
    {
        /// <summary>
        /// Loads an image from disk.
        /// </summary>
        Task<BitmapImage> LoadAsync(string path);

        /// <summary>
        /// Renders the background + overlay and writes two PNGs:
        ///   • primaryPath: composite of background+overlay
        ///   • overlayPath (optional): overlay alone
        /// Returns the actual paths written.
        /// </summary>
        Task<(string primaryPath, string? overlayPath)> SaveAsync(
            FrameworkElement overlay,
            BitmapImage background,
            string primaryPath,
            string? overlayPath = null);
    }
}

ImageService.cs:

//BackgroundBuilder\Services\ImageService.cs
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<BitmapImage> 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));
        }

        /// <summary>
        /// Renders a <paramref name="background"/> plus the <paramref name="mainGrid"/>
        /// at bottomright offset by <paramref name="offset"/>. If background is null,
        /// only the mainGrid is rendered at (0,0).
        /// </summary>
        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;
        }

        /// <summary>
        /// Encodes the given bitmap as PNG and writes it to disk.
        /// </summary>
        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);
        }
    }
}

ITaskbarService.cs:

//BackgroundBuilder\Services\ITaskbarService.cs
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();
        /// <summary>Returns true when the taskbar is horizontal, false when vertical.</summary>
        bool IsHorizontal();
    }
}

TaskbarService.cs:

//BackgroundBuilder\Services\TaskbarService.cs
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<APPBARDATA>() };
            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<APPBARDATA>() };
            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;
        }
    }
}

ObservableObject.cs:

//BackgroundBuilder\Utils\ObservableObject.cs
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));
    }
}

RelayCommand.cs:

//BackgroundBuilder\Utils\RelayCommand.cs
using System;
using System.Windows.Input;

namespace BackgroundBuilder.Utils
{
    public class RelayCommand(Action<object?> execute, Func<object?, bool>? canExecute = null) : ICommand
    {
        private readonly Action<object?> _execute = execute;
        private readonly Func<object?, bool>? _canExecute = canExecute;

        public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true;
        public void Execute(object? parameter) => _execute(parameter);

        public event EventHandler? CanExecuteChanged;
        public void RaiseCanExecuteChanged()
            => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }
}

MainWindowViewModel.cs:

//BackgroundBuilder\ViewModels\MainWindowViewModel.cs
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using BackgroundBuilder.Models;
using BackgroundBuilder.Repositories;
using BackgroundBuilder.Services;
using BackgroundBuilder.Utils;
using Microsoft.Win32;

namespace BackgroundBuilder.ViewModels
{
    public class MainWindowViewModel : ObservableObject
    {
        private readonly IContatoRepository _repo;
        private readonly IImageService _imageService;
        private readonly ITaskbarService _taskbarService;

        public ObservableCollection<Contato> RawContatos { get; } = [];
        public ObservableCollection<Contato> Comando { get; } = [];
        public ObservableCollection<Contato> ContatosSemCMD { get; } = [];
        public ObservableCollection<Contato> Aniversarios { get; } = [];


        private Contato? _selectedContato;
        public Contato? SelectedContato{ get => _selectedContato; set { _selectedContato = value; OnPropertyChanged(); DeleteCommand.RaiseCanExecuteChanged(); } }

        private ObservableCollection<Contato>? _selectedContatos;
        public ObservableCollection<Contato> SelectedContatos { get => _selectedContatos ?? RawContatos; set { _selectedContatos = value; OnPropertyChanged(); UpdateCommand.RaiseCanExecuteChanged(); } }

        private string? _caminhoBGImage;
        public string? CaminhoBGImage{ get => _caminhoBGImage; set { _caminhoBGImage = value; OnPropertyChanged(); }}

        private BitmapImage? _backgroundImage;
        public BitmapImage? BackgroundImage{ get => _backgroundImage; private set { _backgroundImage = value; OnPropertyChanged(); }}
        
        private int _taskbarSize;
        public int TaskbarSize{ get => _taskbarSize; private set { _taskbarSize = value; OnPropertyChanged(); }}

        private string _orientation = "Horizontal";
        public string Orientation{ get => _orientation; private set { _orientation = value; OnPropertyChanged(); }}

        public FrameworkElement? OverlayElement { get; set; }

        // Commands
        public RelayCommand DeleteCommand { get; }
        public RelayCommand AddCommand { get; }
        public RelayCommand SelectImageCommand { get; }
        public ICommand RefreshCommand { get; }
        public RelayCommand UpdateCommand { get; }
        public RelayCommand ExportImageCommand { get; }

        public MainWindowViewModel(
            IContatoRepository repo,
            IImageService imageService,
            ITaskbarService taskbarService)
        {
            _repo = repo;
            _imageService = imageService;
            _taskbarService = taskbarService;

            RefreshTaskbarInfo();

            DeleteCommand = new RelayCommand(async _ => await DeleteAsync(), _ => SelectedContato != null);
            AddCommand = new RelayCommand(async _ => await AddContactAsync());
            SelectImageCommand = new RelayCommand(async _ => await SelectBackground());
            RefreshCommand = new RelayCommand(async _ => await LoadRawAsync());
            UpdateCommand = new RelayCommand(async _ => await UpdateAsync(), _ => SelectedContatos != null);
            ExportImageCommand = new RelayCommand(async _ => await RenderImageAsync(), _ => BackgroundImage != null && OverlayElement != null);
        }

        public void RefreshTaskbarInfo()
        {
            TaskbarSize = _taskbarService.GetTaskbarSize();
            Orientation = _taskbarService.IsHorizontal() ? "Horizontal" : "Vertical";
        }
        
        private async Task DeleteAsync()
        {
            if (SelectedContato == null) return;

            var result = MessageBox.Show(
                "Delete selected record?",
                "Confirm Delete",
                MessageBoxButton.YesNo,
                MessageBoxImage.Question);

            if (result != MessageBoxResult.Yes) return;

            try
            {
                await _repo.DeleteAsync(SelectedContato.Ramal);
                RawContatos.Remove(SelectedContato);
                ApplyFilters();
            }
            catch
            {
                MessageBox.Show(
                    "Error deleting record.",
                    "Delete Failed",
                    MessageBoxButton.OK,
                    MessageBoxImage.Error);
            }
        }

        private async Task AddContactAsync()
        {
            var novo = new Contato { Ramal = "", Nome = "" };
            RawContatos.Add(novo);
            SelectedContato = novo; // Set the newly added contact as the selected one  
            await Task.CompletedTask; // preserve async signature  
        }
           
        private async Task SelectBackground()
        {
            var dlg = new OpenFileDialog
            {
                Filter = "Image Files|*.png;*.jpg;*.jpeg;*.bmp"
            };

            if (dlg.ShowDialog() == true)
            {
                CaminhoBGImage = dlg.FileName;
                BackgroundImage = await _imageService.LoadAsync(dlg.FileName);
                if (!string.IsNullOrEmpty(CaminhoBGImage))
                {
                    ExportImageCommand.RaiseCanExecuteChanged();
                }
            }
        }

        public async Task LoadRawAsync()
        {
            var all = await _repo.GetAllAsync();
            RawContatos.Clear();
            foreach (var c in all.Where(x => x.IsComando)) RawContatos.Add(c);
            foreach (var c in all.Where(x => !x.IsComando).OrderBy(x => x.Nome)) RawContatos.Add(c);

            ApplyFilters();
        }

        private async Task RenderImageAsync()
        {
            var dlg = new SaveFileDialog
            {
                Filter = "PNG Image|*.png"
            };

            if (dlg.ShowDialog() == true
                && OverlayElement is FrameworkElement overlay
                && BackgroundImage is BitmapImage bg)
            {
                await _imageService.SaveAsync(overlay, bg, dlg.FileName, dlg.FileName.Replace(".", "_1."));
            }
        }

        public async Task UpdateAsync()
        {
            if (SelectedContatos == null) return;

            var result = MessageBox.Show(
                "Save selected records?",
                "Confirm Save",
                MessageBoxButton.YesNo,
                MessageBoxImage.Question);

            if (result != MessageBoxResult.Yes) return;

            foreach (var contato in SelectedContatos)
            {
                if (string.IsNullOrWhiteSpace(contato.Ramal)
                    || string.IsNullOrWhiteSpace(contato.Nome))
                {
                    MessageBox.Show(
                        "Ramal and Nome are required.",
                        "Validation Error",
                        MessageBoxButton.OK,
                        MessageBoxImage.Warning);
                    return;
                }

                try
                {
                    await _repo.InsertUpdateAsync(contato);
                }
                catch
                {
                    MessageBox.Show(
                        "Error saving record.",
                        "Save Failed",
                        MessageBoxButton.OK,
                        MessageBoxImage.Error);
                }
            }
            await LoadRawAsync();
            MessageBox.Show(
                "Concluído!",
                "Confirm Save",
                MessageBoxButton.OK,
                MessageBoxImage.Information);
        }

        private void ApplyFilters()
        {
            Comando.Clear();
            ContatosSemCMD.Clear();
            Aniversarios.Clear();

            foreach (var item in RawContatos.Where(x => x.IsComando))
                Comando.Add(item);

            foreach (var item in RawContatos.Where(x => !x.IsComando).OrderBy(x => x.Nome))
                ContatosSemCMD.Add(item);

            foreach (var item in RawContatos.Where(x => !x.IsComando).OrderBy(x => (x.Aniversario ?? DateTime.MinValue).Day).OrderBy(x => (x.Aniversario ?? DateTime.MinValue).Month))
                Aniversarios.Add(item);

        }
    }
}

MainWindow.xaml

<!--BackgroundBuilder\Views\MainWindow.xaml-->
<Window x:Class="BackgroundBuilder.Views.MainWindow"
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:converters="clr-namespace:BackgroundBuilder.Converters"
        Title="BackgroundBuilder" WindowStartupLocation="CenterScreen" WindowState="Maximized" MinHeight="950" MinWidth="1230">
    <Window.Resources>
        <converters:DateNoDotConverter x:Key="DateNoDotConverter" />
    </Window.Resources>
    <Grid x:Name="RootGrid" Margin="20">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        
        <!-- Row 0: Background selection -->
        <Grid Grid.Row="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <!-- Row 2: Image-->
            <Rectangle Grid.Column="0"
                Fill="Gainsboro"
                Height="90"
                Width="160"
                >
            </Rectangle>
            <Image Source="{Binding BackgroundImage}" Height="90" Stretch="Uniform" Grid.Column="0"/>
            <Button Content="Selecionar Imagem de Fundo…" 
                    Command="{Binding SelectImageCommand}" 
                    Grid.Column="1" 
                    Padding="20,3,20,3"
                    Height="30"
                    Margin="5"/>
            <TextBox Text="{Binding CaminhoBGImage}"
                     Grid.Column="2" 
                     IsReadOnly="True"
                     Padding="20,3,20,3"
                     Height="30"
                     Margin="5"/>
        </Grid>
        
        <!-- Row 1: three side-by-side DataGrids -->
        <Grid Grid.Row="1" HorizontalAlignment="Right" VerticalAlignment="Bottom">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>


            <Grid Grid.Column="0" x:Name="MainGrid" HorizontalAlignment="Right" VerticalAlignment="Bottom">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="Auto"/>
                </Grid.ColumnDefinitions>

                <!-- Comando -->
                <DataGrid Grid.Column="0"
                          ItemsSource="{Binding Comando}"
                          AutoGenerateColumns="False"
                          CanUserAddRows="False">
                    <DataGrid.Columns>
                        <DataGridTextColumn Header="Ação" Binding="{Binding Nome}"/>
                        <DataGridTextColumn Header="Ramal" Binding="{Binding Ramal}"/>
                    </DataGrid.Columns>
                </DataGrid>
                <!-- ContatosSemCMD -->
                <DataGrid Grid.Column="1"
                          ItemsSource="{Binding ContatosSemCMD}"
                          AutoGenerateColumns="False"
                          CanUserAddRows="False">
                    <DataGrid.Columns>
                        <DataGridTextColumn Header="Nome" Binding="{Binding Nome}"/>
                        <DataGridTextColumn Header="Email" Binding="{Binding Email}"/>
                        <DataGridTextColumn Header="Ramal" Binding="{Binding Ramal}"/>
                        <DataGridTextColumn Header="Área" Binding="{Binding Area}"/>
                    </DataGrid.Columns>
                </DataGrid>
                <!-- Aniversarios -->
                <DataGrid Grid.Column="2"
                          ItemsSource="{Binding Aniversarios}"
                          AutoGenerateColumns="False"
                          CanUserAddRows="False">
                    <DataGrid.Columns>
                        <DataGridTextColumn Header="Nome" Binding="{Binding Nome}"/>
                        <DataGridTextColumn Header="Aniversário"
                                            Binding="{Binding Aniversario,Converter={StaticResource DateNoDotConverter}}"/>
                    </DataGrid.Columns>
                </DataGrid>
            </Grid>

            <!-- Row 3: DataGrid for RawContatos -->
            <DataGrid x:Name="RawGrid"
                    Grid.Column="2"
                    ItemsSource="{Binding RawContatos}"
                    SelectedItem="{Binding SelectedContato}"
                    AutoGenerateColumns="False"
                    IsReadOnly="False"
                    IsHitTestVisible ="True"
                    FontSize="11"
                    VerticalAlignment="Top"
                    HorizontalAlignment="Center"
                    Height="800"
                    AlternatingRowBackground="DarkGray">
                <DataGrid.ColumnHeaderStyle>
                    <Style TargetType="DataGridColumnHeader">
                        <Setter Property="HorizontalContentAlignment" Value="Center"/>
                        <Setter Property="Background" Value="Gray"/>
                        <Setter Property="Foreground" Value="White"/>
                        <Setter Property="Padding" Value="5"/>
                        <Setter Property="HorizontalAlignment" Value="Stretch"/>
                    </Style>
                </DataGrid.ColumnHeaderStyle>
                <!-- your existing columns here -->
                <DataGrid.Columns>
                    <DataGridTextColumn Header="Ramal"   Binding="{Binding Ramal, UpdateSourceTrigger=PropertyChanged}" />
                    <DataGridTextColumn Header="Nome"    Binding="{Binding Nome, UpdateSourceTrigger=PropertyChanged}" />
                    <DataGridTextColumn Header="Email"   Binding="{Binding Email, UpdateSourceTrigger=PropertyChanged}" />
                    <DataGridTextColumn Header="Área"    Binding="{Binding Area, UpdateSourceTrigger=PropertyChanged}" />
                    <DataGridTemplateColumn Header="Aniversário">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <DatePicker SelectedDate="{Binding Aniversario, UpdateSourceTrigger=PropertyChanged}"/>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                    <DataGridCheckBoxColumn Header="É Comando?" Binding="{Binding IsComando}" />

                </DataGrid.Columns>
            </DataGrid>
        </Grid>

        <!-- Row 3: Buttons for actions -->
        <Grid Grid.Row="3"
              Margin="5">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Button Content="Recarregar Contatos"
                    Command="{Binding RefreshCommand}"
                    Grid.Column="0"
                    FontWeight="Medium"
                    Padding="5"
                    Margin="5"/>
            <Button Content="+ Nova linha" 
                    Command="{Binding AddCommand}" 
                    Grid.Column = "1"
                    Foreground="Green"
                    FontWeight="Medium"
                    Padding="5"
                    Margin="5"/>
            <Button Content="- Deletar selecionada" 
                    Command="{Binding DeleteCommand}"
                    Grid.Column = "2"
                    Foreground="Red" 
                    FontWeight="Medium"
                    Padding="5"
                    Margin="5"/>
            <Button Content="Salvar dados" 
                    Command="{Binding UpdateCommand}"
                    CommandParameter="RawGrid"
                    Grid.Column = "3" 
                    FontWeight="Medium"
                    Padding="5"
                    Margin="5"/>
            <Button Content="Criar Imagem -->" 
                    Command="{Binding ExportImageCommand}" 
                    CommandParameter="MainGrid"
                    Grid.Column = "4"
                    Foreground="Blue"
                    FontWeight="Bold"
                    Padding="5"
                    Margin="5"/>
        </Grid>
    </Grid>
</Window>

MainWindow.xaml.cs:

//BackgroundBuilder\Views\MainWindow.xaml.cs
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using BackgroundBuilder.Models;
using BackgroundBuilder.ViewModels;

namespace BackgroundBuilder.Views
{
    public partial class MainWindow : Window
    {
        private readonly MainWindowViewModel _vm;

        public MainWindow(MainWindowViewModel vm)
        {
            InitializeComponent();

            this.Language = System.Windows.Markup.XmlLanguage.GetLanguage(System.Globalization.CultureInfo.CurrentUICulture.IetfLanguageTag);

            _vm = vm;
            DataContext = vm;

            // Point the VMs OverlayElement to our DataGrid
            _vm.OverlayElement = MainGrid;

            // Load contatos on window load
            Loaded += async (_, __) => await _vm.LoadRawAsync();
        }
    }
}

App.xaml

<!--BackgroundBuilder\App.xaml-->
<Application x:Class="BackgroundBuilder.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             Startup="OnStartup">
    <Application.Resources>
        <Style TargetType="DataGrid">
            <Setter Property="AutoGenerateColumns" Value="True"/>
            <Setter Property="CanUserAddRows" Value="False"/>
            <Setter Property="CanUserDeleteRows" Value="False"/>
            <Setter Property="CanUserReorderColumns" Value="False"/>
            <Setter Property="CanUserResizeColumns" Value="False"/>
            <Setter Property="CanUserSortColumns" Value="False"/>
            <Setter Property="GridLinesVisibility" Value="None"/>
            <Setter Property="BorderThickness" Value="0"/>
            <Setter Property="BorderBrush" Value="Black"/>
            <Setter Property="IsReadOnly" Value="True"/>
            <Setter Property="FontSize" Value="16"/>
            <Setter Property="RowBackground" Value="GhostWhite"/>
            <Setter Property="AlternatingRowBackground" Value="#387f79"/>
            <Setter Property="Margin" Value="5"/>
            <Setter Property="FontStyle" Value="Italic" />
            <Setter Property="FontWeight" Value="Medium" />
            <Setter Property="HeadersVisibility" Value="Column" />
            <Setter Property="TextBlock.TextAlignment" Value="Center"/>
            <Setter Property="HorizontalContentAlignment" Value="Center"/>
            <Setter Property="VerticalAlignment" Value="Bottom"/>
            <Setter Property="IsHitTestVisible" Value="False"/>
        </Style>
        <Style TargetType="DataGridColumnHeader">
            <Setter Property="HorizontalContentAlignment" Value="Center"/>
            <Setter Property="Background" Value="#387f79"/>
            <Setter Property="Foreground" Value="White"/>
            <Setter Property="Padding" Value="5"/>
            <Setter Property="HorizontalAlignment" Value="Stretch"/>
        </Style>
        <Style TargetType="DataGridCell">
            <Setter Property="FontWeight" Value="Medium" />
            <Setter Property="Margin" Value="10,0,10,0"/>
            <Setter Property="HorizontalAlignment" Value="Stretch"/>
        </Style>
        <Style TargetType="DataGridRow">
            <Setter Property="Foreground" Value="Black"/>
            <Style.Triggers>
                <Trigger Property="ItemsControl.AlternationIndex" Value="1">
                    <Setter Property="Foreground" Value="White"/>
                </Trigger>
            </Style.Triggers>
        </Style>
        <Style TargetType="Button">
            <Setter Property="FontSize" Value="16"/>
        </Style>
    </Application.Resources>
</Application>

App.xaml.cs:

//BackgroundBuilder\App.xaml.cs
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<DatabaseService>();
                    services.AddScoped<IContatoRepository, PostgresContatoRepository>();

                    // New image service
                    services.AddSingleton<IImageService, ImageService>();
                    
                    // New taskbar service
                    services.AddSingleton<ITaskbarService, TaskbarService>();

                    // VM & View
                    services.AddTransient<MainWindowViewModel>();
                    services.AddTransient<MainWindow>();
                })
                .Build();
        }

        private async void OnStartup(object sender, StartupEventArgs e)
        {
            await _host.StartAsync();
            var window = _host.Services.GetRequiredService<MainWindow>();
            window.Show();
        }

        protected override async void OnExit(ExitEventArgs e)
        {
            using (_host) { await _host.StopAsync(); }
            base.OnExit(e);
        }
    }
}

appsettings.json:

//BackgroundBuilder\appsettings.json
{
    "ConnectionStrings": {
        "ContatosDb": "Host=192.168.10.248;Username=postgres;Password=gds21;Database=Smart Energia"
    }
}

README.md:

# BackgroundBuilder

[![.NET](https://img.shields.io/badge/.NET-8.0-blueviolet?logo=dotnet&logoColor=white)](https://dotnet.microsoft.com/) [![WPF](https://img.shields.io/badge/WPF-%23C8C8C8?logo=windows&logoColor=blue)](https://learn.microsoft.com/dotnet/desktop/wpf/) [![XAML](https://img.shields.io/badge/XAML-0C54C2?logo=xaml&logoColor=white)](https://learn.microsoft.com/dotnet/desktop/wpf/xaml/) [![C#](https://img.shields.io/badge/C%23-239120?logo=c-sharp&logoColor=white)](https://learn.microsoft.com/dotnet/csharp/) [![MVVM](https://img.shields.io/badge/Pattern-MVVM-ff69b4)](https://learn.microsoft.com/pt-br/dotnet/architecture/maui/mvvm) [![DI](https://img.shields.io/badge/DI-Microsoft.Extensions.Hosting-0078D7?logo=azure-devops&logoColor=white)](https://learn.microsoft.com/dotnet/core/extensions/dependency-injection) [![PostgreSQL](https://img.shields.io/badge/DB-PostgreSQL-4169E1?logo=postgresql&logoColor=white)](https://www.postgresql.org/) [![Npgsql](https://img.shields.io/badge/Driver-Npgsql-008bb9?logo=postgresql&logoColor=white)](https://www.npgsql.org/) [![Dapper](https://img.shields.io/badge/ORM-Dapper-0089D6)](https://github.com/DapperLib/Dapper) [![Windows](https://img.shields.io/badge/Platform-Windows-0078D6?logo=windows&logoColor=white)](https://www.microsoft.com/windows)

## 📝 Project description

> 🖥️ An MVVM WPF application (.NET 8) providing an Excel-like editor for the `contatos` table in PostgreSQL

---

## 📑 Prerequisites

- [![.NET](https://img.shields.io/badge/.NET_8.0-blueviolet?logo=dotnet&logoColor=white)](https://dotnet.microsoft.com/)
- [![PostgreSQL](https://img.shields.io/badge/PostgreSQL-4169E1?logo=postgresql&logoColor=white)](https://www.postgresql.org/) **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:

    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 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

🚀 Releases

📆 21/05/2025: 🆕 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 WPFs RenderTargetBitmap and PngBitmapEncoder under the hood.