# winex.ctrl.listview 外部列表视图

通过窗口消息与远程内存访问外部进程（兼容 64/32 位）的标准 `SysListView32` 列表视图控件。对象以 `win.ui.ctrl.listview` 为基础，重新实现了需要跨进程传递结构体、文本缓冲区的常用操作。

`winex.desktop` 就使用了这个库：`winex.desktop.listview` 是绑定到 Windows 桌面图标列表的 `winex.ctrl.listview` 对象。

## 快速开始：绑定外部列表

构造参数是**列表控件自身的窗口句柄**，不是主窗口句柄、进程 ID 或 TreeView 的节点句柄。

```aardio
import winex;
import winex.ctrl.listview;
import console;

// 按实际情况替换窗口标题。
// findExists 返回值 1 是父窗口句柄，返回值 2 才是匹配的控件句柄。
var hwndParent,hwndList = winex.findExists("目标窗口标题",,,"SysListView32");
if(!hwndList){
    console.log("未找到目标列表控件");
    return;
}

var lv = winex.ctrl.listview(hwndList);
console.log("项目数：",lv.count);

for row,text in lv.each(){
    console.log(row,text);
}

lv.process.free(); // 只释放本次绑定持有的进程资源，不关闭外部窗口。
```

如果已经取得控件句柄，可以直接调用 `winex.ctrl.listview(hwndList)`。
窗口重新创建后句柄可能改变，需要重新查找、绑定。
调用方需要有权打开目标进程并读写其内存；目标以管理员身份运行时，通常也需要相应权限。

## 行列索引与文本读取

常用读写接口的行号、列号从 **1** 开始。

| 接口 | 说明 |
| --- | --- |
| `count` | 项目总数 |
| `columnCount` | 报表视图的列数，通过列表头获取；图标视图不一定有可用的列表头 |
| `getItemText(row,col=1,len=100)` | 读取单个单元格的 Unicode 文本 |
| `each(i=0,flags,col)` | 从指定项目之后开始遍历，依次返回行号、指定列文本；默认遍历全部项目、读取第一列 |
| `getItemRect(row,col,rc,code=0)` | 获取项目或子项矩形；省略 `col` 取整项范围，省略 `rc` 自动创建矩形 |

`getItemText` 的 `len` 是**包含末尾终结符空间的 UTF-16 单元数**，不是 UTF-8 字节数。
默认值为 100，长文本应显式增大；当前实现不会自动扩容。
空文本返回 `""`，不能仅据此判断项目是否存在。

外部绑定的 `getItemText` 每次只读取一列，**不支持用 `col=-1` 读取整行**。
读取报表视图的全部列时，请逐列调用：

```aardio
// lv 为已经绑定的 winex.ctrl.listview 对象。
var readRow = function(lv,row){
    var cells = [];
    var columns = lv.columnCount;
    for(col=1;columns){
        table.push(cells,lv.getItemText(row,col,260));
    }
    return cells;
};
```

矩形坐标相对于列表控件客户区，单位是运行时的设备像素。
可使用 `win.toScreen(lv.hwnd,x,y)` 转为屏幕坐标，不要直接当作屏幕坐标点击。

## 遍历、查找与选中

- `each()`：遍历全部项目，返回 `(row,text)`。
- `each(0,,2)`：遍历全部项目，读取第二列。
- `eachSelected(col)`：遍历选中项目，返回行号与指定列文本。
- `selected`：选中行号数组，适合多选列表。
- `getSelection(i,col)`：查找起始行之后的下一个选中项，未找到返回 0。
- `getFocused(col)` / `selIndex`：当前内部焦点项的行号，未找到返回 0；焦点项不等同于全部选中项。
- `findItem(str,start=-1,sub=true,wrap=true)`：按项目主文本查找，返回 1 起始的行号，未找到返回 0。`sub` 控制部分文本匹配，`wrap` 控制是否从头继续查找；它不是任意子项列查询接口。

**查找起点的例外：**当前 `findItem` 将 `start` 原样传给 Win32。
`start=-1` 表示从首项开始；指定其他值时，它是 **0 起始、排除自身的原生项目索引**，不要与返回的 1 起始行号混用。

```aardio
// 只定义操作函数；调用时传入自己允许修改选择状态的列表对象。
var selectByText = function(lv,text){
    var row = lv.findItem(text,-1,false,false); // 精确匹配，从首项开始，不回绕。
    if(!row) return 0;

    lv.setSelected(row);   // 设置选中状态，不保证清除其他行的选中状态。
    lv.ensureVisible(row); // 滚动到该项。
    return row;
};
```

`setSelected(row,false)` 取消指定行的选中状态；`selIndex=row` 设置内部焦点及选中状态。
这些操作会影响目标程序的界面，并可能触发目标程序自己的事件处理。

## 修改列表

仅在明确允许修改的目标上使用这些接口，建议先在自己创建的测试列表中验证。

| 接口 | 说明 |
| --- | --- |
| `setItemText(text,row,col=1)` | 修改指定单元格；显式传入有效行号 |
| `setItemText(["第一列","第二列"],row)` | 从第一列开始批量写入该行，数组写法不使用第三个列号参数 |
| `addItem(text,ind,...)` | 插入项目，可传多列文本数组；省略位置追加，返回新增行号 |
| `delItem(row)` / `clear()` | 删除指定项 / 全部项目 |
| `setItemState(row,state=0,mask)` | 按掩码设置状态；省略掩码使用 `state`，清除状态位时应显式指定掩码 |
| `getItemState(row,mask=0)` | 按掩码读取状态，读取特定位时应显式传入掩码 |
| `getChecked(row)` / `setChecked(row,value=true)` | 读取 / 修改复选框状态，仅适用于本来就支持复选框的列表 |
| `setItemPos(row,x,y)` | 设置图标项在客户区的位置；自动排列或对齐到网格可能影响结果 |

插入、删除、重命名以及修改样式可能破坏目标程序自身的数据模型。
尤其不要把桌面当作普通测试列表执行 `clear()`、`delItem()` 或批量改名。

## winex.desktop：直接访问桌面图标

`winex.desktop` 会查找桌面的 `SHELLDLL_DefView`，再取得其中的 `SysListView32` 控件，使用 `winex.ctrl.listview` 进行绑定。
因此下面的 `lv` 不是另一套桌面专用列表 API，而是可直接使用上述接口的列表对象。

```aardio
import winex.desktop;
import console;

var lv = winex.desktop.listview;
console.log("桌面图标数：",lv.count);

// 只读枚举，不移动图标，也不修改选择或排列方式。
for(row=1;lv.count){
    var text = lv.getItemText(row,1,260);
    var rc = lv.getItemRect(row);
    console.log(row,text,rc.left,rc.top,rc.right,rc.bottom);
}
```

桌面图标通常按图标视图显示，读取显示名称用第一列即可，不要依赖 `columnCount` 枚举桌面图标。
如果确实需要调整图标位置，可使用 `lv.setItemPos(row,x,y)`；需先考虑桌面的自动排列、对齐到网格设置，并自行保存原位置以便恢复。

`winex.desktop.listview` 是库级共享对象，不要随手调用其 `process.free()`，以免影响后续桌面操作。
Explorer 重启后，旧的控件句柄可能失效，需要重新获取桌面控件。
创建、查找或管理桌面快捷方式文件请使用 `fsys.lnk`，而不是这个窗口控件绑定。

## 跨进程边界与资源释放

- **位数适配：**库根据目标进程位数选择对应的结构体布局，并在目标进程内分配文本缓冲区；常见用途包括从 32 位 aardio 读取 64 位 Explorer 的桌面图标文本。ListView 主要使用固定宽度的项目索引，与 TreeView 的指针宽度节点句柄不同。
- **不是所有继承方法都适合跨进程：**继承了 `win.ui.ctrl.listview` 的方法和属性，并不代表所有本地控件功能都已完成远程适配。优先使用本文列出的常用接口；依赖本地指针、图像列表、通知结构或本地窗口子类化的操作不能直接照搬。
- **不要直接使用本地批量读取实现：**需要批量获取文本时，使用 `getItemText` 循环读取；不要假定继承的 `items`、`getTable` 等内部结构体操作可安全用于外部进程。
- **事件与自绘：**目标控件的 `onnotify`、`onSelChanged`、自绘等通知发给它自己的父窗口，不会因为创建绑定对象就转发给调用方。
- **控件类型限制：**仅适用于标准 `SysListView32`。外观相似的网页表格、WPF、Qt、自绘或其他非标准列表不能直接套用；虚拟列表、回调文本等行为也由目标程序决定。
- **释放自己的绑定：**独立创建的绑定不再使用时，调用 `lv.process.free()` 释放进程句柄及其管理的远程内存。此后不要继续通过该对象进行远程操作。
- **不要用 `lv.close()` 释放资源：**它操作的是目标窗口。当前列表绑定也没有 TreeView 封装额外提供的 `free()`，应使用 `lv.process.free()`。
