# aardio 范例: aria2 下载 —— 如果 BT 下载没速度，先找个热门种子下载就可以了



```aardio
// BT 下载 -自绘 listview 风格
// BT 下载 -自绘卡片风格: https://www.aardio.com/zh-cn/docs/examples/Network/Transfer/bt.draw.html

import fonts.fontAwesome;
import win.ui;
/*DSG{{*/
var winform = win.form(text="aria2 下载 —— 如果 BT 下载没速度，先找个热门种子下载就可以了";right=921;bottom=537;bgcolor=0xFFFFFF)
winform.add(
btnNext={cls="plus";text='\uF054';left=200;top=490;right=240;bottom=520;color=0x3C3C3C;db=1;dl=1;font=LOGFONT(h=-14;name='FontAwesome');notify=1;z=6};
btnPrev={cls="plus";text='\uF053';left=90;top=490;right=130;bottom=520;color=0x3C3C3C;db=1;dl=1;font=LOGFONT(h=-14;name='FontAwesome');notify=1;z=4};
pie={cls="plus";text="100%";left=25;top=480;right=73;bottom=528;bgcolor=0x4A68E8;color=0xFFFFFF;db=1;dl=1;forecolor=0x4AD4A9;z=2};
plusList={cls="plus";left=12;top=7;right=912;bottom=471;db=1;dl=1;dr=1;dt=1;notify=1;z=1};
txtPage={cls="plus";text="1/1";left=135;top=490;right=195;bottom=520;db=1;dl=1;z=5};
txtUrl={cls="plus";text="https://webtorrent.io/torrents/big-buck-bunny.torrent";left=358;top=497;right=906;bottom=523;align="right";border={bottom=1;color=0x808080};db=1;dl=1;dr=1;editable="edit";notify=1;paddingTop=5;textPadding={right=24;bottom=1};z=3}
)
/*}}*/

// 下载任务列表数据管理
var taskList = {
    items = {};
    gidToIndex = {};
    selectedIndex = 0;
    hoverIndex = 0;
    currentPage = 1;
    pageSize = 8;
    rowHeight = 50;
    headerHeight = 40;
    
    columns = {
        {"文件", 0.32},
        {"速度", 0.12},
        {"已下载", 0.18},
        {"状态", 0.26},
        {"连接数", 0.12}
    };
    
    getTotalPages = function(){
        // ★ 只计算未移除的任务
        var count = 0;
        for(i,item in owner.items){
            if(!item.isRemoving) count++;
        }
        return math.max(1, math.ceil(count / owner.pageSize));
    };
    
    getPageItems = function(){
        var visibleItems = {};
        for(i,item in owner.items){
            if(!item.isRemoving){
                table.push(visibleItems, item);
            }
        }
        
        var startIdx = (owner.currentPage - 1) * owner.pageSize + 1;
        var endIdx = math.min(startIdx + owner.pageSize - 1, #visibleItems);
        var result = {};
        for(i=startIdx; endIdx){
            table.push(result, visibleItems[i]);
        }
        return result, startIdx;
    };
    
    addTask = function(gid, name){
        var item = {
            gid = gid;
            name = name || "未知文件";
            speed = "";
            downloaded = "";
            total = "";
            status = "等待中";
            connections = "";
            percent = 0;
            isComplete = false;
            isError = false;
            isRemoving = false;  // ★ 新增
        };
        table.push(owner.items, item);
        owner.gidToIndex[gid] = #owner.items;
        return #owner.items;
    };
    
    getItemByGid = function(gid){
        var idx = owner.gidToIndex[gid];
        if(idx) return owner.items[idx], idx;
    };
    
    // ★ 标记为正在移除（不真正删除）
    markRemoving = function(gid){
        var item = owner.getItemByGid(gid);
        if(item){
            item.isRemoving = true;
            item.status = "正在移除...";
            item.speed = "";
        }
    };
    
    // ★ 真正删除（在 aria2 确认停止后调用）
    removeByGid = function(gid){
        var idx = owner.gidToIndex[gid];
        if(!idx) return;
        table.remove(owner.items, idx);
        owner.gidToIndex[gid] = null;
        for(i=idx; #owner.items){
            owner.gidToIndex[owner.items[i].gid] = i;
        }
        if(owner.currentPage > owner.getTotalPages()){
            owner.currentPage = owner.getTotalPages();
        }
    };
    
    updateTask = function(gid, data){
        var item = owner.getItemByGid(gid);
        if(!item) return;
        // ★ 正在移除的任务不更新（除非是要取消移除）
        if(item.isRemoving && !data.isRemoving) return;
        for(k,v in data){
            item[k] = v;
        }
    };
    
    hitTest = function(x, y, rc){
        if(y < owner.headerHeight) return 0, null;
        var row = math.floor((y - owner.headerHeight) / owner.rowHeight) + 1;
        
        // ★ 基于可见任务计算
        var pageItems = owner.getPageItems();
        if(row > 0 && row <= #pageItems){
            return row, pageItems[row];
        }
        return 0, null;
    };
};

winform.afterDpiChanged = function(scaleX,scaleY,origScaleX,origScaleY){
	taskList.rowHeight = 30 * scaleY;  
	taskList.headerHeight = 25 * scaleY;   
}

var updatePageText = function(){
	winform.txtPage.text = string.format("%d/%d", taskList.currentPage, taskList.getTotalPages());
};

// 翻页按钮样式
winform.btnPrev.skin({
	background = { hover=0x20000000; active=0x30000000 };
	color = { default=0xFF3C3C3C; hover=0xFF0078D4; active=0xFF005A9E; disabled=0xFFAAAAAA }
});
winform.btnNext.skin({
	background = { hover=0x20000000; active=0x30000000 };
	color = { default=0xFF3C3C3C; hover=0xFF0078D4; active=0xFF005A9E; disabled=0xFFAAAAAA }
});

winform.btnPrev.oncommand = function(){
	if(taskList.currentPage > 1){
		taskList.currentPage--;
		taskList.selectedIndex = 0;
		updatePageText();
		winform.plusList.redraw();
	}
};

winform.btnNext.oncommand = function(){
	if(taskList.currentPage < taskList.getTotalPages()){
		taskList.currentPage++;
		taskList.selectedIndex = 0;
		updatePageText();
		winform.plusList.redraw();
	}
};

winform.plusList.onDrawBackground = function(graphics, rc){
	var brush = gdip.solidBrush(0xFFFFFFFF);
	graphics.fillRectangle(brush, rc);
	brush.delete();
	
	var pen = gdip.pen(0xFFE0E0E0, 1);
	graphics.drawRectangle(pen, rc.left, rc.top, rc.right - rc.left - 1, rc.bottom - rc.top - 1);
	pen.delete();
};

winform.plusList.onDrawForeground = function(graphics, rc, foregroundRect){
	graphics.smoothingMode = 4;
	
	var scaleX, scaleY, dpiScaleX, dpiScaleY = winform.getScale();
	var rcWidth = rc.right - rc.left;
	var rcHeight = rc.bottom - rc.top;
	
	var colWidths = {};
	var totalRatio = 0;
	for(i,col in taskList.columns){
		totalRatio += col[2];
	}
	var availWidth = rcWidth - 20;
	for(i,col in taskList.columns){
		colWidths[i] = availWidth * col[2] / totalRatio;
	}
	
	// 创建字体
	var fontFamily = gdip.family("Tahoma");//注意 XP 以后的系统 GDI+ 会自动将 Tahoma 修改为 "Segoe UI";
	var headerFont = fontFamily.createFont(10, 0, 3/*_UnitPoint*/);
	var itemFont = fontFamily.createFont(9, 0, 3/*_UnitPoint*/);
	var smallFont = fontFamily.createFont(8, 0, 3/*_UnitPoint*/); // ★ 进度条内使用更小字体
	var strFormat = gdip.stringFormat();
	strFormat.trimming = 3/*_StringTrimmingEllipsisCharacter*/;
	
	// 绘制表头背景
	var headerBrush = gdip.solidBrush(0xFFF8F8F8);
	graphics.fillRectangle(headerBrush, 1, 1, rcWidth - 2, taskList.headerHeight - 1);
	headerBrush.delete();
	
	var headerPen = gdip.pen(0xFFE0E0E0, 1);
	graphics.drawLine(headerPen, 0, taskList.headerHeight, rcWidth, taskList.headerHeight);
	headerPen.delete();
	
	// 绘制表头文字
	var headerTextBrush = gdip.solidBrush(0xFF555555);
	var xPos = 10;
	for(i,col in taskList.columns){
		var textRc = ::RECTF(xPos, 0, colWidths[i], taskList.headerHeight);
		strFormat.lineAlign = 1/*_StringAlignmentCenter*/;
		graphics.drawString(col[1], headerFont, textRc, strFormat, headerTextBrush);
		xPos += colWidths[i];
	}
	headerTextBrush.delete();
	
	var pageItems, startIdx = taskList.getPageItems();
	
	var itemTextBrush = gdip.solidBrush(0xFF333333);
	var grayTextBrush = gdip.solidBrush(0xFF888888);
	
	// ★ 计算文本行高度（用于垂直居中）
	var textHeight = 18 * dpiScaleY;
	
	for(i, item in pageItems){
		var globalIdx = startIdx + i - 1;
		var yPos = taskList.headerHeight + (i - 1) * taskList.rowHeight;
		
		if(globalIdx == taskList.selectedIndex){
			var selBrush = gdip.solidBrush(0xFFE3F2FD);
			graphics.fillRectangle(selBrush, 1, yPos, rcWidth - 2, taskList.rowHeight);
			selBrush.delete();
		}
		elseif(globalIdx == taskList.hoverIndex){
			var hoverBrush = gdip.solidBrush(0xFFFAFAFA);
			graphics.fillRectangle(hoverBrush, 1, yPos, rcWidth - 2, taskList.rowHeight);
			hoverBrush.delete();
		}
		
		var rowPen = gdip.pen(0xFFF0F0F0, 1);
		graphics.drawLine(rowPen, 10, yPos + taskList.rowHeight, rcWidth - 10, yPos + taskList.rowHeight);
		rowPen.delete();
		
		xPos = 10;
		// ★ 文本垂直居中
		var textY = yPos + (taskList.rowHeight - textHeight) / 2;
		
		// 第1列：文件名
		var nameRc = ::RECTF(xPos, textY, colWidths[1] - 5, textHeight);
		strFormat.lineAlign = 1;
		strFormat.align = 0;
		graphics.drawString(item.name, itemFont, nameRc, strFormat, itemTextBrush);
		xPos += colWidths[1];
		
		// 第2列：速度
		var speedRc = ::RECTF(xPos, textY, colWidths[2] - 5, textHeight);
		graphics.drawString(item.speed, itemFont, speedRc, strFormat, grayTextBrush);
		xPos += colWidths[2];
		
		// 第3列：已下载
		var dlText = item.downloaded;
		if(#item.total) dlText = item.downloaded + "/" + item.total;
		var dlRc = ::RECTF(xPos, textY, colWidths[3] - 5, textHeight);
		graphics.drawString(dlText, itemFont, dlRc, strFormat, grayTextBrush);
		xPos += colWidths[3];
		
		// 第4列：状态/进度条
		var statusWidth = colWidths[4] - 10;
		var progressHeight = 16 * dpiScaleY;
		var progressY = yPos + (taskList.rowHeight - progressHeight) / 2;
		
		if(item.isComplete){
			var completeBrush = gdip.solidBrush(0xFF4CAF50);
			var completeRc = ::RECTF(xPos, textY, statusWidth, textHeight);
			graphics.drawString("✓ 已完成", itemFont, completeRc, strFormat, completeBrush);
			completeBrush.delete();
		}
		elseif(item.isError){
			var errorBrush = gdip.solidBrush(0xFFE53935);
			var errorRc = ::RECTF(xPos, textY, statusWidth, textHeight);
			graphics.drawString(item.status, itemFont, errorRc, strFormat, errorBrush);
			errorBrush.delete();
		}
		elseif(item.percent > 0 || item.status == "下载中"){
			var barRadius = 4;  // ★ 略微增加圆角
			
			// 进度条背景
			var bgPath = gdip.path();
			bgPath.addRoundRect(::RECTF(xPos, progressY, statusWidth, progressHeight), barRadius);
			var bgBrush = gdip.solidBrush(0xFFE8E8E8);
			graphics.fillPath(bgBrush, bgPath);
			bgBrush.delete();
			bgPath.delete();
			
			// 进度条前景
			if(item.percent > 0){
				var fgWidth = statusWidth * item.percent / 100;
				if(fgWidth > barRadius * 2){
					var fgPath = gdip.path();
					fgPath.addRoundRect(::RECTF(xPos, progressY, fgWidth, progressHeight), barRadius);
					var p1 = ::POINTF(xPos, progressY);
					var p2 = ::POINTF(xPos + statusWidth, progressY);
					var fgBrush = gdip.lineBrush(p1, p2, 0xFF42A5F5, 0xFF1E88E5, 0);
					graphics.fillPath(fgBrush, fgPath);
					fgBrush.delete();
					fgPath.delete();
				}
			}
			
			// ★ 进度文字：使用更小字体，确保不会被截断
			var percentText = string.format("%.1f%%", item.percent);
			var percentRc = ::RECTF(xPos, progressY, statusWidth, progressHeight);
			strFormat.align = 1/*_StringAlignmentCenter*/;
			strFormat.lineAlign = 1/*_StringAlignmentCenter*/;
			var percentBrush = gdip.solidBrush(item.percent > 50 ? 0xFFFFFFFF : 0xFF555555);
			graphics.drawString(percentText, smallFont, percentRc, strFormat, percentBrush); // ★ 使用 smallFont
			percentBrush.delete();
			strFormat.align = 0;
		}
		else {
			var statusRc = ::RECTF(xPos, textY, statusWidth, textHeight);
			graphics.drawString(item.status, itemFont, statusRc, strFormat, grayTextBrush);
		}
		xPos += colWidths[4];
		
		// 第5列：连接数
		var connRc = ::RECTF(xPos, textY, colWidths[5] - 10, textHeight);
		graphics.drawString(tostring(item.connections), itemFont, connRc, strFormat, grayTextBrush);
	}
	
	if(#pageItems == 0){
		var emptyBrush = gdip.solidBrush(0xFFAAAAAA);
		var emptyRc = ::RECTF(0, taskList.headerHeight, rcWidth, rcHeight - taskList.headerHeight);
		strFormat.align = 1;
		strFormat.lineAlign = 1;
		graphics.drawString("暂无下载任务", itemFont, emptyRc, strFormat, emptyBrush);
		emptyBrush.delete();
	}
	
	itemTextBrush.delete();
	grayTextBrush.delete();
	strFormat.delete();
	headerFont.delete();
	itemFont.delete();
	smallFont.delete(); 
	fontFamily.delete();
};

winform.plusList.onMouseMove = function(wParam, lParam){
	var x, y = win.getMessagePos(lParam);
	var rc = owner.getClientRect();
	var newHover = taskList.hitTest(x, y, rc);
	if(newHover != taskList.hoverIndex){
		taskList.hoverIndex = newHover;
		owner.redraw();
	}
};

winform.plusList.onMouseLeave = function(){
	if(taskList.hoverIndex != 0){
		taskList.hoverIndex = 0;
		owner.redraw();
	}
};

winform.plusList.onMouseUp = function(wParam, lParam){
	var x, y = win.getMessagePos(lParam);
	var rc = owner.getClientRect();
	var clickIdx = taskList.hitTest(x, y, rc);
	if(clickIdx > 0 && clickIdx != taskList.selectedIndex){
		taskList.selectedIndex = clickIdx;
		owner.redraw();
	}
};

var btnAdd = winform.txtUrl.addCtrl(
	cls="plus";
	marginRight=0;marginBottom=2;width=24;
	iconText = '\uF019';
	iconStyle={
		align="right";font=LOGFONT(h=-15;name='FontAwesome');padding={top=3}
	}
);

btnAdd.skin({
	color = {
		default = 0xC0000000;
		hover = 0xFFFF0000;
		active = 0xFF00FF00;
	};
});

winform.pie.setPieRange(0, 100);
winform.show(); //先显示窗口避免等待

import process.aria2;
var aria2 = process.aria2();
aria2.startServer(maxConcurrentDownloads = 10);// 启动 aria2 下载服务进程

var aria2Ui = {
	addTaskInfo = function(gid, url, status){
		if(!gid){
			import bencode;
			var name = bencode.magnet.getName(url);
			taskList.addTask("error_" + time.tick(), name || "未知文件");
			var item = taskList.items[#taskList.items];
			item.status = "出错了：" + (status || "未知错误");
			item.isError = true;
		}
		else {
			var existItem = taskList.getItemByGid(gid);
			if(!existItem){
				var taskName = aria2.taskName(gid);
				taskList.addTask(gid, taskName || "未知文件");
			}
			if(status){
				taskList.updateTask(gid, {status = status});
			}
		}
		updatePageText();
		winform.plusList.redraw();
	};
	
	remove = function(gid){
		taskList.removeByGid(gid);
		updatePageText();
		winform.plusList.redraw();
	};
	
	updateStatus = function(gid, status){
		var item = taskList.getItemByGid(gid);
		if(!item){
			owner.addTaskInfo(gid);
		}
		taskList.updateTask(gid, {status = status});
		winform.plusList.redraw();
	};
	
	updateProgress = function(gid, progress){
		var item = taskList.getItemByGid(gid);
		if(item && item.isRemoving) return;
		 
		if(!item){
			owner.addTaskInfo(gid);
			item = taskList.getItemByGid(gid);
		}
		
		if(progress){
			var percent = 0;
			progress.totalLength = tonumber(progress.totalLength);
			if( progress.totalLength > 0){
				percent = progress.completedLength / progress.totalLength * 100;
			}
			taskList.updateTask(gid, {
				speed = math.size64(progress.downloadSpeed).format() + "/s";
				downloaded = math.size64(progress.completedLength).format();
				total = math.size64(progress.totalLength).format();
				connections = progress.connections;
				percent = percent;
				status = "下载中";
			});
		}
		else {
			var info = aria2.tellStatus(gid, "totalLength");
			taskList.updateTask(gid, {
				speed = "";
				total = math.size64(info.totalLength).format();
				connections = "";
			});
		}
		winform.plusList.redraw();
	};
};

aria2.onDownloadStart = function(task){
	aria2Ui.addTaskInfo(task.gid, , "开始下载");
};

aria2.onDownloadPause = function(task){
	taskList.updateTask(task.gid, {status = "已暂停"; speed = ""});
	winform.plusList.redraw();
};

aria2.onDownloadStop = function(task){
    var item = taskList.getItemByGid(task.gid);
    if(item && item.isRemoving){
        // ★ 确认已移除，真正删除
        taskList.removeByGid(task.gid);
    }
    else {
        taskList.updateTask(task.gid, {status = "已停止"; speed = ""});
    }
    updatePageText();
    winform.plusList.redraw();
};

aria2.onDownloadComplete = function(task){
	if(aria2.taskIsTorrent(task.gid)){
		aria2Ui.remove(task.gid);
	}
	else {
		taskList.updateTask(task.gid, {
			isComplete = true;
			speed = "";
			percent = 100;
		});
		aria2Ui.updateProgress(task.gid);
	}
};

aria2.onDownloadError = function(task){
	var errMsg = aria2.taskErrorMessage(task.gid);
	taskList.updateTask(task.gid, {
		status = errMsg || "下载出错";
		isError = true;
		speed = "";
	});
	winform.plusList.redraw();
};

winform.plusList.onContextMenu = function(x, y){
	var cx, cy = win.toClient(owner.hwnd, x, y);
	var rc = owner.getClientRect();
	var clickIdx = taskList.hitTest(cx, cy, rc);
	
	if(clickIdx <= 0) return;
	taskList.selectedIndex = clickIdx;
	owner.redraw();
	
	var item = taskList.items[clickIdx];
	if(!item) return;
	
	var gid = item.gid;
	var statusInfo = aria2.tellStatus(gid, "status", "belongsTo");
	if(!statusInfo) return;
	
	import win.ui.menu;
	var popmenu = win.ui.popmenu(winform);
	
	popmenu.add('移除', function(){
		aria2.taskRemove(gid);
		taskList.markRemoving(gid);
		
		updatePageText();
		winform.plusList.redraw();
	});
	
	if(statusInfo.status == "active"){
		popmenu.add('暂停', function(){
			aria2.taskPause(gid);
		});
	}
	if(statusInfo.status == "paused"){
		popmenu.add('开始下载', function(){
			aria2.taskUnpause(gid);
		});
	}
	
	var path = aria2.taskFilePath(gid);
	if(#path){
		popmenu.add('浏览文件', function(){
			raw.explore(path, "/select");
		});
	}
	
	popmenu.add('复制链接', function(){
		var url = aria2.taskUrl(gid);
		if(url){
			import win.clip;
			win.clip.write(url);
		}
	});
	
	popmenu.popup(x, y, true);
};

btnAdd.oncommand = function(){
	var url = winform.txtUrl.text;
	if(!#url){
		winform.msgboxErr("请输入下载地址或种子文件路径");
		winform.txtUrl.setFocus();
		return;
	}
	
	aria2.taskAdd(url, function(gid, err){
		if(err){
			aria2Ui.addTaskInfo(null, url, err[["message"]]);
		}
		else {
			aria2Ui.addTaskInfo(gid, url, "添加成功");
		}
	});
};

winform.txtUrl.editBox.onOk = function(ctrl, alt, shift){
	btnAdd.oncommand();
	return true;
};

import com.interface.ITaskbarList3;
var taskbar = com.interface.ITaskbarList3.Create();

updateDownloadStatus = function(init){
	aria2.tellActive(function(result, err){
		if(result){
			var totalLength, completedLength = math.size64(), math.size64();
			for(k, v in result){
				totalLength.add(v.totalLength);
				completedLength.add(v.completedLength);
				aria2Ui.updateProgress(v.gid, v);
			}
			
			var pos = 0;
			if(tonumber(totalLength) > 0){
				pos = (completedLength / totalLength) * 100;
			}
			winform.pie.progressPos = pos;
			winform.pie.text = math.round(pos,pos<10?1:0)+"%";
			taskbar.SetProgressValue(winform.hwnd, pos, 100);
		}
	}, "gid", "status", "connections", "downloadSpeed", "totalLength", "completedLength");
	
	if(init){
		aria2.tellWaiting(0, 20, function(result, err){
			if(result){
				for(i, v in result){
					aria2Ui.updateProgress(v.gid, v);
				}
			}
		}, "gid", "status", "connections", "downloadSpeed", "totalLength", "completedLength");
	}
};

aria2.onError = function(errMsg, rpcErr){
	
};

aria2.ready(function(){
	updateDownloadStatus(true);
	winform.setInterval(updateDownloadStatus, 1500);
});

winform.onDestroy = function(){
	aria2.stop();
};

win.loopMessage();
```

