Adicionar arquivos de projeto.

This commit is contained in:
Giuliano Paschoalino 2025-05-15 09:39:58 -03:00
parent f5f3b40120
commit b979de68c5
22 changed files with 2146 additions and 0 deletions

53
App.xaml Normal file
View File

@ -0,0 +1,53 @@
<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>

54
App.xaml.cs Normal file
View File

@ -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<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);
}
}
}

10
AssemblyInfo.cs Normal file
View File

@ -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)
)]

29
BackgroundBuilder.csproj Normal file
View File

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows7.0</TargetFramework>
<UseWPF>true</UseWPF>
<RootNamespace>BackgroundBuilder</RootNamespace>
<AssemblyName>BackgroundBuilder</AssemblyName>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.0-preview.3.25171.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0-preview.3.25171.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0-preview.3.25171.5" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-preview.3.25171.5" />
<PackageReference Include="Npgsql" Version="9.0.3" />
<PackageReference Include="System.Drawing.Common" Version="10.0.0-preview.3.25173.2" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

22
BackgroundBuilder.sln Normal file
View File

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

View File

@ -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();
}
}

14
Models/Contato.cs Normal file
View File

@ -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; }
}
}

46
README.md Normal file
View File

@ -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 WPFs `RenderTargetBitmap` and `PngBitmapEncoder` under the hood.
---

View File

@ -0,0 +1,13 @@
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);
}
}

View File

@ -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<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);
}
}
}

View File

@ -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;
}
}
}

26
Services/IImageService.cs Normal file
View File

@ -0,0 +1,26 @@
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);
}
}

View File

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

115
Services/ImageService.cs Normal file
View File

@ -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<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);
}
}
}

View File

@ -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<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;
}
}
}

12
Utils/ObservableObject.cs Normal file
View File

@ -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));
}
}

18
Utils/RelayCommand.cs Normal file
View File

@ -0,0 +1,18 @@
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);
}
}

View File

@ -0,0 +1,229 @@
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);
}
}
}

184
Views/MainWindow.xaml Normal file
View File

@ -0,0 +1,184 @@
<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>

29
Views/MainWindow.xaml.cs Normal file
View File

@ -0,0 +1,29 @@
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();
}
}
}

5
appsettings.json Normal file
View File

@ -0,0 +1,5 @@
{
"ConnectionStrings": {
"ContatosDb": "Host=192.168.10.248;Username=postgres;Password=gds21;Database=Smart Energia"
}
}

1104
resume.md Normal file

File diff suppressed because it is too large Load Diff